test(harness): reorganize regression suites and preserve contract coverage

This commit is contained in:
2026-10-16 07:24:49 +00:00
parent b6f87db623
commit 33709ab873
108 changed files with 5869 additions and 5328 deletions
@@ -0,0 +1,30 @@
"""Actual collector invocation coverage, separate from documentation contracts."""
from pathlib import Path
import importlib.util
import unittest
from harness.tests._support.paths import HARNESS
class ActualCollectorTests(unittest.TestCase):
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()
@@ -0,0 +1,67 @@
"""Documentation contracts only; runtime collector coverage is separate."""
from pathlib import Path
import re
import unittest
from harness.tests._support.paths import HARNESS
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 ('content_version', 'updated_at'):
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('post-comment snapshot을 body update 직전 baseline', authoring)
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 ('해소:', '잔여:', '신규:', '문서 보완:', '해당 없음'):
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 이전', 'inspect.signature', '.bind('):
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', 'flags='):
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('\\]\\(([^)]+)\\)', text):
if not link.startswith(('http:', 'https:', '#')):
self.assertTrue((path.parent / link.split('#')[0]).exists(), link)
if __name__ == "__main__":
unittest.main()
@@ -5,7 +5,7 @@ import sys
import tempfile
from pathlib import Path
import unittest
from harness.tests.spec import test_implementation_spec_review_support as fixture
from harness.tests._support import spec as fixture
class RequestedReviewerProjectionTests(unittest.TestCase):
@@ -168,9 +168,9 @@ class RequestedReviewerProjectionTests(unittest.TestCase):
def test_document_declares_version_default_and_narrow_collection_scope(self):
skill = fixture.MODPATH.parent.parent / 'SKILL.md'
text = skill.read_text()
for token in ('requested_reviewers_presentation_order', '1.27.3',
'생략하면 `false`', 'Raw before/after snapshot', 'POST 재시도 금지'):
with self.subTest(token=token): self.assertIn(token, text)
for token in ('requested_reviewers_presentation_order', '1.27.3'):
with self.subTest(token=token):
self.assertIn(token, text)
if __name__ == '__main__':
unittest.main()
@@ -4,7 +4,7 @@ import subprocess
import sys
import tempfile
import unittest
from harness.tests.spec import test_implementation_spec_review_support as fixture
from harness.tests._support import spec as fixture
class SourceIssueReferenceProjectionTests(unittest.TestCase):
@@ -0,0 +1,134 @@
import copy
import json
import os
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from harness.tests._support.paths import ROOT
from harness.tests._support.spec import MODPATH, mod, publication_fixture, result, verify_fixture
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'):
self.assertIn(rule, section)
if __name__ == "__main__":
unittest.main()
+74
View File
@@ -0,0 +1,74 @@
import copy
import json
import os
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from harness.tests._support.paths import ROOT
from harness.tests._support.spec import MODPATH, mod, publication_fixture, result, verify_fixture
class CacheTests(unittest.TestCase):
def test_cache_validation_and_rejects_symlink(self):
with tempfile.TemporaryDirectory(dir="/opt/data/cache/scratch") as td:
root = Path(td); ops = root / "harness" / "operations"; ops.mkdir(parents=True)
contract = ops / "repository-cache.md"; contract.write_text("cache", encoding="utf-8")
rs = root / "repositories"; owner = rs / "acme"; repo = owner / "repo"
(repo / "locks").mkdir(parents=True)
(repo / "repository.git").mkdir()
for p in (rs, owner, repo, repo / "locks"): os.chmod(p, 0o700)
info = mod.validate_cache_paths(contract, "acme", "repo", 3)
self.assertTrue(info["repository_git"].endswith("repository.git"))
(repo / "locks" / "issue-3.lock").symlink_to(contract)
with self.assertRaises(mod.ValidationError):
mod.validate_cache_paths(contract, "acme", "repo", 3)
def test_cache_enforces_root_boundary_uid_and_dangling_locks(self):
with tempfile.TemporaryDirectory(dir="/opt/data/cache/scratch") as td:
root = Path(td); ops = root / "harness" / "operations"; ops.mkdir(parents=True)
contract = ops / "repository-cache.md"; contract.write_text("cache", encoding="utf-8")
rs = root / "repositories"; owner = rs / "acme"; repo = owner / "repo"
(repo / "locks").mkdir(parents=True); (repo / "repository.git").mkdir()
for path in (rs, owner, repo, repo / "locks"):
path.chmod(0o700)
os.chmod(root, 0o777)
with self.assertRaises(mod.ValidationError):
mod.validate_cache_paths(contract, "acme", "repo", 3)
os.chmod(root, 0o700)
with self.assertRaises(mod.ValidationError):
mod.validate_cache_paths(contract, "acme", "repo", 3, uid=os.getuid() + 10000)
(repo / "locks" / "issue-3.lock").symlink_to(root / "missing")
with self.assertRaises(mod.ValidationError):
mod.validate_cache_paths(contract, "acme", "repo", 3)
(repo / "locks" / "issue-3.lock").unlink()
alias = root / "alias"; alias.symlink_to(ops.parent, target_is_directory=True)
with self.assertRaises(mod.ValidationError):
mod.validate_cache_paths(alias / "operations" / "repository-cache.md", "acme", "repo", 3)
def test_cache_group_membership_includes_other_primary_accounts(self):
from types import SimpleNamespace
from unittest.mock import patch
role = SimpleNamespace(pw_name="role", pw_uid=os.getuid(), pw_gid=111)
other = SimpleNamespace(pw_name="other", pw_uid=os.getuid() + 1, pw_gid=111)
group = SimpleNamespace(gr_gid=111, gr_mem=["role"])
with tempfile.TemporaryDirectory(dir="/opt/data/cache/scratch") as td:
root = Path(td); ops = root / "harness" / "operations"; ops.mkdir(parents=True)
contract = ops / "repository-cache.md"; contract.write_text("cache")
rs = root / "repositories"; repo = rs / "acme" / "repo"
(repo / "locks").mkdir(parents=True); (repo / "repository.git").mkdir()
for path in (rs / "acme", repo, repo / "locks"):
path.chmod(0o700)
rs.chmod(0o770)
role.pw_gid = other.pw_gid = group.gr_gid = rs.stat().st_gid
with patch.object(mod.grp, "getgrgid", return_value=group), \
patch.object(mod.pwd, "getpwuid", return_value=role), \
patch.object(mod.pwd, "getpwall", return_value=[role, other]):
with self.assertRaises(mod.ValidationError):
mod.validate_cache_paths(contract, "acme", "repo", 3)
if __name__ == "__main__":
unittest.main()
+223
View File
@@ -0,0 +1,223 @@
import copy
import json
import os
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from harness.tests._support.paths import ROOT
from harness.tests._support.spec import MODPATH, mod, publication_fixture, result, verify_fixture
class CodecTests(unittest.TestCase):
def test_validate_and_render_roundtrip(self):
r = result()
self.assertEqual(mod.validate_result(r, {"src/a.py", "tests/test_a.py"}), r)
body = mod.render_review(r, {"src/a.py", "tests/test_a.py"}, declaration={"mode": "none"})
parsed = mod.parse_review(body, {"src/a.py", "tests/test_a.py"})
self.assertEqual(parsed, r)
self.assertEqual(mod.render_review(parsed, {"src/a.py", "tests/test_a.py"}, declaration={"mode": "none"}), body)
def test_validation_rejects_bool_path_traversal_bad_verdict(self):
r = result()
r["target"]["issue"] = True
with self.assertRaises(mod.ValidationError):
mod.validate_result(r, {"src/a.py"})
r = result(); r["criteria"][0]["implementation_paths"] = ["../x"]
with self.assertRaises(mod.ValidationError):
mod.validate_result(r, {"../x"})
r = result("request_changes")
with self.assertRaises(mod.ValidationError):
mod.validate_result(r, {"src/a.py", "tests/test_a.py"})
def test_collect_pages_requires_identity_and_reads_all_pages(self):
pages = [[{"id": 1}, {"id": 2}], [{"id": 3}]]
def fetch(n):
return {"items": pages[n - 1], "total_count": 3, "has_more": n == 1}
self.assertEqual(mod.collect_pages(fetch, "id", page_size=2), pages[0] + pages[1])
def bad(n):
return {"items": [{"id": 1}], "total_count": 2, "has_more": True}
with self.assertRaises(mod.ValidationError):
mod.collect_pages(bad, "id", max_pages=1)
def test_verify_unchanged(self):
self.assertTrue(mod.verify_unchanged({"a": [1]}, {"a": [1]}))
with self.assertRaises(mod.ValidationError):
mod.verify_unchanged({"a": 1}, {"a": 2})
def test_cli_check_pages_and_unchanged(self):
with tempfile.TemporaryDirectory(dir="/opt/data/cache/scratch") as td:
td = Path(td); env = dict(os.environ, PYTHONDONTWRITEBYTECODE="1")
pages = td / "pages.json"; pages.write_text(json.dumps([
{"items": [{"id": 1}], "total_count": 1, "has_more": False}],), encoding="utf-8")
base = [sys.executable, str(MODPATH)]
p = subprocess.run(base + ["check-pages", "--pages", str(pages), "--key", "id"], capture_output=True, text=True, env=env)
self.assertEqual(p.returncode, 0, p.stderr)
b = td / "before.json"; a = td / "after.json"
b.write_text('{"x":1}', encoding="utf-8"); a.write_text('{"x":1}', encoding="utf-8")
p = subprocess.run(base + ["check-unchanged", "--before", str(b), "--after", str(a)], capture_output=True, text=True, env=env)
self.assertEqual(p.returncode, 0, p.stderr); self.assertIn('"unchanged": true', p.stdout)
def test_cli_validate_render_parse(self):
with tempfile.TemporaryDirectory(dir="/opt/data/cache/scratch") as td:
td = Path(td); rf = td / "r.json"; af = td / "a.json"; bf = td / "b.md"
rf.write_text(json.dumps(result(), ensure_ascii=False), encoding="utf-8")
af.write_text(json.dumps(["src/a.py", "tests/test_a.py"]), encoding="utf-8")
env = dict(os.environ, PYTHONDONTWRITEBYTECODE="1")
base = [sys.executable, str(MODPATH)]
p = subprocess.run(base + ["validate-result", "--result", str(rf), "--allowed-paths", str(af)],
text=True, capture_output=True, env=env)
self.assertEqual(p.returncode, 0, p.stderr)
p = subprocess.run(base + ["render", "--no-delegated-evidence", "--result", str(rf), "--allowed-paths", str(af)],
text=True, capture_output=True, env=env)
self.assertEqual(p.returncode, 0, p.stderr); bf.write_text(p.stdout, encoding="utf-8")
p = subprocess.run(base + ["parse", "--body", str(bf), "--allowed-paths", str(af)],
text=True, capture_output=True, env=env)
self.assertEqual(p.returncode, 0, p.stderr)
self.assertEqual(json.loads(p.stdout), result())
def test_result_codec_preserves_existing_native_format(self):
r = result()
paths = {"src/a.py", "tests/test_a.py"}
body = mod.render_review(r, paths, declaration={"mode": "none"})
self.assertIn(" - 구현 근거: src/a.py; 증거\n", body)
self.assertIn("모든 기준을 충족했다.\n\n<!--", body)
self.assertEqual(mod.parse_review(body, paths), r)
unordered = json.loads(json.dumps(r, sort_keys=True))
self.assertEqual(mod.render_review(unordered, paths, declaration={"mode": "none"}), body)
def test_strict_codec_rejects_unrenderable_and_sensitive_human_values(self):
paths = {"src/a.py", "tests/test_a.py"}
for field, value in (("verdict", []), ("conclusion", "trailing "),
("conclusion", "line\u2028break"), ("conclusion", "ftp://example.test")):
r = result(); r[field] = value
with self.subTest(field=field, value=value), self.assertRaises(mod.ValidationError):
mod.validate_result(r, paths)
for field in ("criterion", "contract", "evidence", "reason"):
r = result(); r["criteria"][0][field] = "secretvalue"
with self.subTest(field=field), self.assertRaises(mod.ValidationError):
mod.validate_result(r, paths, sensitive_values=("secretvalue",))
r = result(); r["verdict"] = "blocked_spec"; r["scope"]["spec_conflicts"] = ["secretvalue"]
with self.assertRaises(mod.ValidationError):
mod.validate_result(r, paths, sensitive_values=("secretvalue",))
with self.assertRaises(mod.ValidationError):
mod.loads_json('{"n":1e999}')
with self.assertRaises(mod.ValidationError):
mod.validate_result(result(), "src/a.py")
def test_parser_rejects_forged_marker_and_body_drift(self):
paths = {"src/a.py", "tests/test_a.py"}
body = mod.render_review(result(), paths, declaration={"mode": "none"})
for bad in (body.replace("issue=3", "issue=4"), body.replace("증거", "다른 증거"),
body.replace("\n", "\r\n"), body + "\n", body.replace("correlation=", "other=")):
with self.subTest(body=bad[-100:]), self.assertRaises(mod.ValidationError):
mod.parse_review(bad, paths)
def test_codec_roundtrips_delimiter_bearing_paths_and_prose(self):
r = result(); r["criteria"][0]["implementation_paths"] = ["src/a, b.py", "src/c; d.py"]
r["criteria"][0]["evidence"] = "확인; 모두 — 일치"
r["verdict"] = "blocked_spec"; r["scope"]["spec_conflicts"] = ["하나, 둘", "세 가지"]
paths = {"src/a, b.py", "src/c; d.py", "tests/test_a.py"}
self.assertEqual(mod.parse_review(mod.render_review(r, paths, declaration={"mode": "none"}), paths), r)
def test_read_only_replay_uses_recursive_exact_json_types(self):
with self.assertRaises(mod.ValidationError):
mod.verify_unchanged({"nested": [True]}, {"nested": [1]})
def test_pagination_is_one_based_and_rejects_short_continuations(self):
calls = []
pages = [{"items": [{"id": 1}, {"id": 2}], "total_count": 3, "has_more": True},
{"items": [{"id": 3}], "total_count": 3, "has_more": False}]
def fetch(page):
calls.append(page)
return pages[page - 1]
self.assertEqual(mod.collect_pages(fetch, "id", page_size=2), [{"id": 1}, {"id": 2}, {"id": 3}])
self.assertEqual(calls, [1, 2])
with self.assertRaises(mod.ValidationError):
mod.collect_pages(lambda _: {"items": [{"id": 1}], "total_count": 3, "has_more": True}, "id", page_size=2)
def test_pagination_validates_all_declared_metadata_and_identity_types(self):
bad_pages = [
{"items": [{"id": 1}], "total_count": 0, "has_more": True},
{"items": [{"id": 1}, {"id": 2}], "total_count": 2, "has_more": True},
{"items": [{"id": []}], "has_more": False},
{"items": [{"id": 1.5}], "has_more": False},
{"items": [{"id": 0}], "has_more": False},
{"items": [{"id": True}], "has_more": False},
{"items": [{"id": 1}, {"id": 1}], "has_more": False},
{"items": None, "has_more": False},
{"items": [], "has_more": True},
]
for page in bad_pages:
with self.subTest(page=bad_pages.index(page)), self.assertRaises(mod.ValidationError):
mod.collect_pages(lambda _: page, "id", page_size=2, max_pages=2)
pages = [{"items": [{"id": 1}], "total_count": 2, "has_more": True},
{"items": [{"id": 2}], "total_count": 3, "has_more": False}]
with self.assertRaises(mod.ValidationError):
mod.collect_pages(lambda i: pages[i - 1], "id", page_size=1)
self.assertEqual(mod.collect_pages(lambda _: {"items": None, "total_count": 0, "has_more": False}, "id"), [])
def test_cli_errors_are_sanitized_without_tracebacks_or_echoed_values(self):
env = dict(os.environ, PYTHONDONTWRITEBYTECODE="1")
base = [sys.executable, str(MODPATH)]
for args in (["unknown-secret-value"], ["check-cache", "--issue", "secret-value"],
["check-unchanged", "--before", "secret-value", "--after", "missing"]):
p = subprocess.run(base + args, capture_output=True, text=True, env=env)
self.assertNotEqual(p.returncode, 0)
self.assertNotIn("secret-value", p.stderr + p.stdout)
self.assertNotIn("Traceback", p.stderr + p.stdout)
self.assertEqual(json.loads(p.stderr), {"error": "invalid input"})
with tempfile.TemporaryDirectory(dir="/opt/data/cache/scratch") as td:
path = Path(td) / "pages.json"
path.write_text(json.dumps([{ "items": [{"id": 1}], "has_more": True, "total_count": 2 }]))
p = subprocess.run(base + ["check-pages", "--pages", str(path), "--key", "id"],
capture_output=True, text=True, env=env)
self.assertNotEqual(p.returncode, 0)
self.assertNotIn("Traceback", p.stderr)
self.assertEqual(json.loads(p.stderr), {"error": "invalid input"})
def test_target_identifiers_preserve_dots_but_reject_marker_breaking_whitespace(self):
r = result(); paths = {"src/a.py", "tests/test_a.py"}
r["target"].update(owner="acme.name", repository="repo.name")
body = mod.render_review(r, paths, declaration={"mode": "none"})
self.assertEqual(mod.parse_review(body, paths), r)
r["target"]["owner"] = "acme name"
with self.assertRaises(mod.ValidationError):
mod.validate_result(r, paths)
def test_rendered_paths_cannot_inject_markdown_html_or_mentions(self):
r = result(); path = "src/<script>@user`[link].py"
r["criteria"][0]["implementation_paths"] = [path]
paths = {path, "tests/test_a.py"}
body = mod.render_review(r, paths, declaration={"mode": "none"})
self.assertNotIn("<script>", body)
self.assertNotIn("@user", body)
self.assertNotIn("`[link]", body)
self.assertEqual(mod.parse_review(body, paths), r)
def test_strict_types_limits_and_duplicate_nested_keys(self):
paths = {"src/a.py", "tests/test_a.py"}
for bad_json in ('{"a":1,"a":2}', '{"a":NaN}',
'{"outer":{"x":1,"x":2}}', '{"x":Infinity}', '{"x":-Infinity}'):
with self.subTest(bad_json=bad_json), self.assertRaises(mod.ValidationError):
mod.loads_json(bad_json)
cases = []
r = result(); r["target"]["pull"] = False; cases.append(r)
r = result(); r["criteria"].append(copy.deepcopy(r["criteria"][0])); cases.append(r)
r = result(); r["criteria"][0]["contract"] = "a" * 4001; cases.append(r)
r = result(); r["criteria"][0]["criterion"] = "a" * 201; cases.append(r)
r = result(); r["criteria"][0]["result"] = {}; cases.append(r)
r = result(); r["criteria"][0]["test_paths"] *= 2; cases.append(r)
r = result(); r["criteria"][0]["test_paths"] = ["/absolute"]; cases.append(r)
r = result(); r["criteria"][0]["test_paths"] = ["tests/../file.py"]; cases.append(r)
r = result(); r["extra"] = None; cases.append(r)
r = result(); del r["scope"]; cases.append(r)
for value in cases:
with self.subTest(case=cases.index(value)), self.assertRaises(mod.ValidationError):
mod.validate_result(value, paths)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,133 @@
import copy
import json
import os
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from harness.tests._support.paths import ROOT
from harness.tests._support.spec import MODPATH, mod, publication_fixture, result, verify_fixture
class PublicationTests(unittest.TestCase):
def test_publication_accepts_one_exact_review_and_rejects_competing_event(self):
before, after, review, r, paths = publication_fixture()
self.assertEqual(verify_fixture(before, after, review, r, paths)["review_id"], 11)
bad = copy.deepcopy(after)
bad["timeline"].append({"id": 102, "review_id": 12, "user": {"id": 42}, "body": review["body"]})
with self.assertRaises(mod.ValidationError):
verify_fixture(before, bad, review, r, paths)
def test_publication_requires_exact_readback_not_only_review_id(self):
before, after, review, r, paths = publication_fixture()
for field, value in (("body", "changed"), ("official", False), ("submitted_at", "bad")):
exact = copy.deepcopy(review); exact[field] = value
with self.subTest(field=field), self.assertRaises(mod.ValidationError):
verify_fixture(before, after, exact, r, paths)
def test_publication_rejects_unrelated_drift_and_bad_projections(self):
before, after, review, r, paths = publication_fixture()
cases = []
for field, value in (("updated_at", "2026-10-03T10:00:00Z"), ("review_comments", 4),
("requested_reviewers", [{"id": 43}]), ("head", {"sha": "d" * 40})):
bad = copy.deepcopy(after); bad["pr"][field] = value; cases.append(bad)
for field, value in (("updated_at", "2026-10-03T10:00:00Z"), ("title", "changed")):
bad = copy.deepcopy(after); bad["issue"][field] = value; cases.append(bad)
bad = copy.deepcopy(after); bad["reviews"][0]["stale"] = 0; cases.append(bad)
bad = copy.deepcopy(after); bad["reviews"][0]["state"] = "PENDING"; cases.append(bad)
bad = copy.deepcopy(after); bad["reviews"].append(copy.deepcopy(review)); cases.append(bad)
bad = copy.deepcopy(after); bad["timeline"][0]["type"] = "comment"; cases.append(bad)
bad = copy.deepcopy(after); bad["timeline"][0]["review_id"] = True; cases.append(bad)
for bad in cases:
with self.subTest(snapshot=cases.index(bad)), self.assertRaises(mod.ValidationError):
verify_fixture(before, bad, bad["reviews"][0], r, paths)
def test_publication_binds_snapshot_repository_issue_and_head(self):
before, after, review, r, paths = publication_fixture()
for section, field, value in (("issue", "number", 4), ("issue", "repository", {"full_name": "wrong/repo"}),
("pr", "head", {"sha": "d" * 40}), ("pr", "number", 8)):
b = copy.deepcopy(before); a = copy.deepcopy(after)
b[section][field] = a[section][field] = value
with self.subTest(section=section, field=field), self.assertRaises(mod.ValidationError):
verify_fixture(b, a, review, r, paths)
def test_publication_preserves_previous_reviews_and_bounds_supersession(self):
before, after, review, r, paths = publication_fixture()
old = copy.deepcopy(review); old.update(id=10, body="previous", commit_id="d" * 40,
state="REQUEST_CHANGES", updated_at="2026-10-03T00:00:00Z")
before["reviews"] = [old]; after["reviews"].insert(0, copy.deepcopy(old))
verify_fixture(before, after, review, r, paths)
after["reviews"][0].update(official=False, dismissed=True, updated_at=review["submitted_at"])
with self.assertRaises(mod.ValidationError):
verify_fixture(before, after, review, r, paths)
contract = {"version": {"version": "1.27.3"}, "supersession_same_author_only": True,
"already_dismissed_review_timestamp_projection": True}
verify_fixture(before, after, review, r, paths, contract)
for mutation in ("body", "user", "missing"):
a = copy.deepcopy(after)
if mutation == "missing": a["reviews"].pop(0)
elif mutation == "user": a["reviews"][0]["user"] = {"id": 43}
else: a["reviews"][0]["body"] = "tampered"
with self.subTest(mutation=mutation), self.assertRaises(mod.ValidationError):
verify_fixture(before, a, review, r, paths, contract)
old["official"] = False; old["dismissed"] = True
after["reviews"][0] = copy.deepcopy(old); after["reviews"][0]["updated_at"] = review["submitted_at"]
verify_fixture(before, after, review, r, paths, contract)
contract["already_dismissed_review_timestamp_projection"] = False
with self.assertRaises(mod.ValidationError):
verify_fixture(before, after, review, r, paths, contract)
def test_publication_preserves_collection_identity_and_normalizes_only_files_order(self):
before, after, review, r, paths = publication_fixture()
second = {"filename": "tests/test_a.py", "status": "added", "additions": 2}
before["files"].append(second); after["files"].insert(0, copy.deepcopy(second))
verify_fixture(before, after, review, r, paths)
after["files"].append(copy.deepcopy(second))
with self.assertRaises(mod.ValidationError):
verify_fixture(before, after, review, r, paths)
def test_cli_verify_publication_and_rejects_raw_crlf_body(self):
before, after, review, r, paths = publication_fixture()
env = dict(os.environ, PYTHONDONTWRITEBYTECODE="1")
with tempfile.TemporaryDirectory(dir="/opt/data/cache/scratch") as td:
root = Path(td)
for name, value in (("before", before), ("after", after), ("exact", review),
("result", r), ("paths", sorted(paths)), ("inline", [])):
(root / (name + ".json")).write_text(json.dumps(value, ensure_ascii=False))
cmd = [sys.executable, str(MODPATH), "verify-publication", "--before", str(root / "before.json"),
"--after", str(root / "after.json"), "--exact-review", str(root / "exact.json"),
"--result", str(root / "result.json"), "--allowed-paths", str(root / "paths.json"),
"--inline-comments", str(root / "inline.json"), "--reviewer-id", "42",
"--expected-pr-url", "https://example.test/acme/repo/pulls/7"]
p = subprocess.run(cmd, capture_output=True, text=True, env=env)
self.assertEqual(p.returncode, 0, p.stderr)
self.assertEqual(json.loads(p.stdout)["review_id"], 11)
body_path = root / "body.md"; body_path.write_bytes(review["body"].replace("\n", "\r\n").encode())
p = subprocess.run([sys.executable, str(MODPATH), "parse", "--body", str(body_path),
"--allowed-paths", str(root / "paths.json")], capture_output=True, text=True, env=env)
self.assertNotEqual(p.returncode, 0)
self.assertEqual(json.loads(p.stderr), {"error": "invalid input"})
def test_publication_accepts_causal_event_timestamp_without_forcing_clock_equality(self):
before, after, review, r, paths = publication_fixture()
event = after["timeline"][0]
event["created_at"] = event["updated_at"] = "2026-10-03T01:24:01Z"
after["pr"]["updated_at"] = event["created_at"]
verify_fixture(before, after, review, r, paths)
def test_publication_requires_independent_canonical_pr_url_anchor(self):
before, after, review, r, paths = publication_fixture()
for foreign in ("https://evil.test/acme/repo/pulls/7", "https://example.test/prefix/acme/repo/pulls/7"):
b = copy.deepcopy(before); a = copy.deepcopy(after)
b["pr"]["html_url"] = b["pr"]["url"] = foreign
a["pr"]["html_url"] = a["pr"]["url"] = foreign
a["reviews"][0]["pull_request_url"] = foreign
a["timeline"][0]["pull_request_url"] = foreign
with self.subTest(url=foreign), self.assertRaises(mod.ValidationError):
verify_fixture(b, a, a["reviews"][0], r, paths)
if __name__ == "__main__":
unittest.main()
@@ -1,584 +0,0 @@
import copy
import importlib.util
import json
import os
import stat
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[3]
MODPATH = ROOT / "harness/agents/spec/skills/gitea-implementation-spec-review/scripts/review_support.py"
spec = importlib.util.spec_from_file_location("review_support", MODPATH)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
def result(verdict="approve"):
return {
"schema": "gitea-agent.implementation-spec-review-result/v1",
"target": {"owner": "acme", "repository": "repo", "issue": 3, "pull": 7,
"head": "a" * 40},
"contract_sha256": "b" * 64,
"verdict": verdict,
"criteria": [{"criterion": "REQ-1", "contract": "계약", "implementation_paths": ["src/a.py"],
"test_paths": ["tests/test_a.py"], "evidence": "증거", "result": "satisfied",
"reason": "충족"}],
"scope": {"missing": [], "excess": [], "spec_conflicts": []},
"conclusion": "모든 기준을 충족했다.",
}
def publication_fixture():
r = result(); paths = {"src/a.py", "tests/test_a.py"}
body = mod.render_review(r, paths, declaration={"mode": "none"})
url = "https://example.test/acme/repo/pulls/7"
user = {"id": 42, "login": "spec-agent"}
review = {"id": 11, "body": body, "commit_id": r["target"]["head"], "state": "APPROVED",
"user": user, "official": True, "stale": False, "dismissed": False,
"comments_count": 0, "submitted_at": "2026-10-03T01:24:00Z",
"updated_at": "2026-10-03T01:24:00Z", "pull_request_url": url}
before = {"issue": {"id": 3, "number": 3, "repository": {"full_name": "acme/repo"},
"updated_at": "2026-10-03T00:00:00Z", "title": "contract"},
"issue_comments": [], "pr": {"id": 7, "number": 7, "html_url": url,
"url": url, "base": {"repo": {"full_name": "acme/repo"}, "sha": "c" * 40},
"head": {"sha": "a" * 40}, "user": {"id": 99}, "state": "open", "merged": False,
"review_comments": 0, "updated_at": "2026-10-03T00:00:00Z", "requested_reviewers": []},
"pr_comments": [], "reviews": [], "timeline": [],
"files": [{"filename": "src/a.py", "status": "modified", "additions": 1}]}
after = copy.deepcopy(before); after["reviews"] = [review]
after["pr"]["updated_at"] = review["submitted_at"]; after["pr"]["review_comments"] = 1
after["timeline"] = [{"id": 101, "review_id": 11, "user": user, "body": body,
"type": "review", "created_at": review["submitted_at"],
"updated_at": review["submitted_at"], "pull_request_url": url}]
return before, after, review, r, paths
def verify_fixture(before, after, review, r, paths, contract=None):
return mod.verify_publication(before, after, review, r, paths, 42, [], contract,
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):
mod.loads_json('{"a":1,"a":2}')
with self.assertRaises(mod.ValidationError):
mod.loads_json('{"a":NaN}')
def test_validate_and_render_roundtrip(self):
r = result()
self.assertEqual(mod.validate_result(r, {"src/a.py", "tests/test_a.py"}), r)
body = mod.render_review(r, {"src/a.py", "tests/test_a.py"}, declaration={"mode": "none"})
parsed = mod.parse_review(body, {"src/a.py", "tests/test_a.py"})
self.assertEqual(parsed, r)
self.assertEqual(mod.render_review(parsed, {"src/a.py", "tests/test_a.py"}, declaration={"mode": "none"}), body)
def test_validation_rejects_bool_path_traversal_bad_verdict(self):
r = result()
r["target"]["issue"] = True
with self.assertRaises(mod.ValidationError):
mod.validate_result(r, {"src/a.py"})
r = result(); r["criteria"][0]["implementation_paths"] = ["../x"]
with self.assertRaises(mod.ValidationError):
mod.validate_result(r, {"../x"})
r = result("request_changes")
with self.assertRaises(mod.ValidationError):
mod.validate_result(r, {"src/a.py", "tests/test_a.py"})
def test_collect_pages_requires_identity_and_reads_all_pages(self):
pages = [[{"id": 1}, {"id": 2}], [{"id": 3}]]
def fetch(n):
return {"items": pages[n - 1], "total_count": 3, "has_more": n == 1}
self.assertEqual(mod.collect_pages(fetch, "id", page_size=2), pages[0] + pages[1])
def bad(n):
return {"items": [{"id": 1}], "total_count": 2, "has_more": True}
with self.assertRaises(mod.ValidationError):
mod.collect_pages(bad, "id", max_pages=1)
def test_verify_unchanged(self):
self.assertTrue(mod.verify_unchanged({"a": [1]}, {"a": [1]}))
with self.assertRaises(mod.ValidationError):
mod.verify_unchanged({"a": 1}, {"a": 2})
def test_cache_validation_and_rejects_symlink(self):
with tempfile.TemporaryDirectory(dir="/opt/data/cache/scratch") as td:
root = Path(td); ops = root / "harness" / "operations"; ops.mkdir(parents=True)
contract = ops / "repository-cache.md"; contract.write_text("cache", encoding="utf-8")
rs = root / "repositories"; owner = rs / "acme"; repo = owner / "repo"
(repo / "locks").mkdir(parents=True)
(repo / "repository.git").mkdir()
for p in (rs, owner, repo, repo / "locks"): os.chmod(p, 0o700)
info = mod.validate_cache_paths(contract, "acme", "repo", 3)
self.assertTrue(info["repository_git"].endswith("repository.git"))
(repo / "locks" / "issue-3.lock").symlink_to(contract)
with self.assertRaises(mod.ValidationError):
mod.validate_cache_paths(contract, "acme", "repo", 3)
def test_publication_accepts_one_exact_review_and_rejects_competing_event(self):
before, after, review, r, paths = publication_fixture()
self.assertEqual(verify_fixture(before, after, review, r, paths)["review_id"], 11)
bad = copy.deepcopy(after)
bad["timeline"].append({"id": 102, "review_id": 12, "user": {"id": 42}, "body": review["body"]})
with self.assertRaises(mod.ValidationError):
verify_fixture(before, bad, review, r, paths)
def test_cli_check_pages_and_unchanged(self):
with tempfile.TemporaryDirectory(dir="/opt/data/cache/scratch") as td:
td = Path(td); env = dict(os.environ, PYTHONDONTWRITEBYTECODE="1")
pages = td / "pages.json"; pages.write_text(json.dumps([
{"items": [{"id": 1}], "total_count": 1, "has_more": False}],), encoding="utf-8")
base = [sys.executable, str(MODPATH)]
p = subprocess.run(base + ["check-pages", "--pages", str(pages), "--key", "id"], capture_output=True, text=True, env=env)
self.assertEqual(p.returncode, 0, p.stderr)
b = td / "before.json"; a = td / "after.json"
b.write_text('{"x":1}', encoding="utf-8"); a.write_text('{"x":1}', encoding="utf-8")
p = subprocess.run(base + ["check-unchanged", "--before", str(b), "--after", str(a)], capture_output=True, text=True, env=env)
self.assertEqual(p.returncode, 0, p.stderr); self.assertIn('"unchanged": true', p.stdout)
def test_cli_validate_render_parse(self):
with tempfile.TemporaryDirectory(dir="/opt/data/cache/scratch") as td:
td = Path(td); rf = td / "r.json"; af = td / "a.json"; bf = td / "b.md"
rf.write_text(json.dumps(result(), ensure_ascii=False), encoding="utf-8")
af.write_text(json.dumps(["src/a.py", "tests/test_a.py"]), encoding="utf-8")
env = dict(os.environ, PYTHONDONTWRITEBYTECODE="1")
base = [sys.executable, str(MODPATH)]
p = subprocess.run(base + ["validate-result", "--result", str(rf), "--allowed-paths", str(af)],
text=True, capture_output=True, env=env)
self.assertEqual(p.returncode, 0, p.stderr)
p = subprocess.run(base + ["render", "--no-delegated-evidence", "--result", str(rf), "--allowed-paths", str(af)],
text=True, capture_output=True, env=env)
self.assertEqual(p.returncode, 0, p.stderr); bf.write_text(p.stdout, encoding="utf-8")
p = subprocess.run(base + ["parse", "--body", str(bf), "--allowed-paths", str(af)],
text=True, capture_output=True, env=env)
self.assertEqual(p.returncode, 0, p.stderr)
self.assertEqual(json.loads(p.stdout), result())
def test_result_codec_preserves_existing_native_format(self):
r = result()
paths = {"src/a.py", "tests/test_a.py"}
body = mod.render_review(r, paths, declaration={"mode": "none"})
self.assertIn(" - 구현 근거: src/a.py; 증거\n", body)
self.assertIn("모든 기준을 충족했다.\n\n<!--", body)
self.assertEqual(mod.parse_review(body, paths), r)
unordered = json.loads(json.dumps(r, sort_keys=True))
self.assertEqual(mod.render_review(unordered, paths, declaration={"mode": "none"}), body)
def test_strict_codec_rejects_unrenderable_and_sensitive_human_values(self):
paths = {"src/a.py", "tests/test_a.py"}
for field, value in (("verdict", []), ("conclusion", "trailing "),
("conclusion", "line\u2028break"), ("conclusion", "ftp://example.test")):
r = result(); r[field] = value
with self.subTest(field=field, value=value), self.assertRaises(mod.ValidationError):
mod.validate_result(r, paths)
for field in ("criterion", "contract", "evidence", "reason"):
r = result(); r["criteria"][0][field] = "secretvalue"
with self.subTest(field=field), self.assertRaises(mod.ValidationError):
mod.validate_result(r, paths, sensitive_values=("secretvalue",))
r = result(); r["verdict"] = "blocked_spec"; r["scope"]["spec_conflicts"] = ["secretvalue"]
with self.assertRaises(mod.ValidationError):
mod.validate_result(r, paths, sensitive_values=("secretvalue",))
with self.assertRaises(mod.ValidationError):
mod.loads_json('{"n":1e999}')
with self.assertRaises(mod.ValidationError):
mod.validate_result(result(), "src/a.py")
def test_parser_rejects_forged_marker_and_body_drift(self):
paths = {"src/a.py", "tests/test_a.py"}
body = mod.render_review(result(), paths, declaration={"mode": "none"})
for bad in (body.replace("issue=3", "issue=4"), body.replace("증거", "다른 증거"),
body.replace("\n", "\r\n"), body + "\n", body.replace("correlation=", "other=")):
with self.subTest(body=bad[-100:]), self.assertRaises(mod.ValidationError):
mod.parse_review(bad, paths)
def test_codec_roundtrips_delimiter_bearing_paths_and_prose(self):
r = result(); r["criteria"][0]["implementation_paths"] = ["src/a, b.py", "src/c; d.py"]
r["criteria"][0]["evidence"] = "확인; 모두 — 일치"
r["verdict"] = "blocked_spec"; r["scope"]["spec_conflicts"] = ["하나, 둘", "세 가지"]
paths = {"src/a, b.py", "src/c; d.py", "tests/test_a.py"}
self.assertEqual(mod.parse_review(mod.render_review(r, paths, declaration={"mode": "none"}), paths), r)
def test_cache_enforces_root_boundary_uid_and_dangling_locks(self):
with tempfile.TemporaryDirectory(dir="/opt/data/cache/scratch") as td:
root = Path(td); ops = root / "harness" / "operations"; ops.mkdir(parents=True)
contract = ops / "repository-cache.md"; contract.write_text("cache", encoding="utf-8")
rs = root / "repositories"; owner = rs / "acme"; repo = owner / "repo"
(repo / "locks").mkdir(parents=True); (repo / "repository.git").mkdir()
for path in (rs, owner, repo, repo / "locks"):
path.chmod(0o700)
os.chmod(root, 0o777)
with self.assertRaises(mod.ValidationError):
mod.validate_cache_paths(contract, "acme", "repo", 3)
os.chmod(root, 0o700)
with self.assertRaises(mod.ValidationError):
mod.validate_cache_paths(contract, "acme", "repo", 3, uid=os.getuid() + 10000)
(repo / "locks" / "issue-3.lock").symlink_to(root / "missing")
with self.assertRaises(mod.ValidationError):
mod.validate_cache_paths(contract, "acme", "repo", 3)
(repo / "locks" / "issue-3.lock").unlink()
alias = root / "alias"; alias.symlink_to(ops.parent, target_is_directory=True)
with self.assertRaises(mod.ValidationError):
mod.validate_cache_paths(alias / "operations" / "repository-cache.md", "acme", "repo", 3)
def test_cache_group_membership_includes_other_primary_accounts(self):
from types import SimpleNamespace
from unittest.mock import patch
role = SimpleNamespace(pw_name="role", pw_uid=os.getuid(), pw_gid=111)
other = SimpleNamespace(pw_name="other", pw_uid=os.getuid() + 1, pw_gid=111)
group = SimpleNamespace(gr_gid=111, gr_mem=["role"])
with tempfile.TemporaryDirectory(dir="/opt/data/cache/scratch") as td:
root = Path(td); ops = root / "harness" / "operations"; ops.mkdir(parents=True)
contract = ops / "repository-cache.md"; contract.write_text("cache")
rs = root / "repositories"; repo = rs / "acme" / "repo"
(repo / "locks").mkdir(parents=True); (repo / "repository.git").mkdir()
for path in (rs / "acme", repo, repo / "locks"):
path.chmod(0o700)
rs.chmod(0o770)
role.pw_gid = other.pw_gid = group.gr_gid = rs.stat().st_gid
with patch.object(mod.grp, "getgrgid", return_value=group), \
patch.object(mod.pwd, "getpwuid", return_value=role), \
patch.object(mod.pwd, "getpwall", return_value=[role, other]):
with self.assertRaises(mod.ValidationError):
mod.validate_cache_paths(contract, "acme", "repo", 3)
def test_publication_accepts_gitea_nested_identity(self):
before, after, review, r, paths = publication_fixture()
self.assertEqual(verify_fixture(before, after, review, r, paths)["review_id"], 11)
def test_publication_requires_exact_readback_not_only_review_id(self):
before, after, review, r, paths = publication_fixture()
for field, value in (("body", "changed"), ("official", False), ("submitted_at", "bad")):
exact = copy.deepcopy(review); exact[field] = value
with self.subTest(field=field), self.assertRaises(mod.ValidationError):
verify_fixture(before, after, exact, r, paths)
def test_publication_rejects_unrelated_drift_and_bad_projections(self):
before, after, review, r, paths = publication_fixture()
cases = []
for field, value in (("updated_at", "2026-10-03T10:00:00Z"), ("review_comments", 4),
("requested_reviewers", [{"id": 43}]), ("head", {"sha": "d" * 40})):
bad = copy.deepcopy(after); bad["pr"][field] = value; cases.append(bad)
for field, value in (("updated_at", "2026-10-03T10:00:00Z"), ("title", "changed")):
bad = copy.deepcopy(after); bad["issue"][field] = value; cases.append(bad)
bad = copy.deepcopy(after); bad["reviews"][0]["stale"] = 0; cases.append(bad)
bad = copy.deepcopy(after); bad["reviews"][0]["state"] = "PENDING"; cases.append(bad)
bad = copy.deepcopy(after); bad["reviews"].append(copy.deepcopy(review)); cases.append(bad)
bad = copy.deepcopy(after); bad["timeline"][0]["type"] = "comment"; cases.append(bad)
bad = copy.deepcopy(after); bad["timeline"][0]["review_id"] = True; cases.append(bad)
for bad in cases:
with self.subTest(snapshot=cases.index(bad)), self.assertRaises(mod.ValidationError):
verify_fixture(before, bad, bad["reviews"][0], r, paths)
def test_publication_binds_snapshot_repository_issue_and_head(self):
before, after, review, r, paths = publication_fixture()
for section, field, value in (("issue", "number", 4), ("issue", "repository", {"full_name": "wrong/repo"}),
("pr", "head", {"sha": "d" * 40}), ("pr", "number", 8)):
b = copy.deepcopy(before); a = copy.deepcopy(after)
b[section][field] = a[section][field] = value
with self.subTest(section=section, field=field), self.assertRaises(mod.ValidationError):
verify_fixture(b, a, review, r, paths)
def test_publication_preserves_previous_reviews_and_bounds_supersession(self):
before, after, review, r, paths = publication_fixture()
old = copy.deepcopy(review); old.update(id=10, body="previous", commit_id="d" * 40,
state="REQUEST_CHANGES", updated_at="2026-10-03T00:00:00Z")
before["reviews"] = [old]; after["reviews"].insert(0, copy.deepcopy(old))
verify_fixture(before, after, review, r, paths)
after["reviews"][0].update(official=False, dismissed=True, updated_at=review["submitted_at"])
with self.assertRaises(mod.ValidationError):
verify_fixture(before, after, review, r, paths)
contract = {"version": {"version": "1.27.3"}, "supersession_same_author_only": True,
"already_dismissed_review_timestamp_projection": True}
verify_fixture(before, after, review, r, paths, contract)
for mutation in ("body", "user", "missing"):
a = copy.deepcopy(after)
if mutation == "missing": a["reviews"].pop(0)
elif mutation == "user": a["reviews"][0]["user"] = {"id": 43}
else: a["reviews"][0]["body"] = "tampered"
with self.subTest(mutation=mutation), self.assertRaises(mod.ValidationError):
verify_fixture(before, a, review, r, paths, contract)
old["official"] = False; old["dismissed"] = True
after["reviews"][0] = copy.deepcopy(old); after["reviews"][0]["updated_at"] = review["submitted_at"]
verify_fixture(before, after, review, r, paths, contract)
contract["already_dismissed_review_timestamp_projection"] = False
with self.assertRaises(mod.ValidationError):
verify_fixture(before, after, review, r, paths, contract)
def test_publication_preserves_collection_identity_and_normalizes_only_files_order(self):
before, after, review, r, paths = publication_fixture()
second = {"filename": "tests/test_a.py", "status": "added", "additions": 2}
before["files"].append(second); after["files"].insert(0, copy.deepcopy(second))
verify_fixture(before, after, review, r, paths)
after["files"].append(copy.deepcopy(second))
with self.assertRaises(mod.ValidationError):
verify_fixture(before, after, review, r, paths)
def test_read_only_replay_uses_recursive_exact_json_types(self):
with self.assertRaises(mod.ValidationError):
mod.verify_unchanged({"nested": [True]}, {"nested": [1]})
def test_pagination_is_one_based_and_rejects_short_continuations(self):
calls = []
pages = [{"items": [{"id": 1}, {"id": 2}], "total_count": 3, "has_more": True},
{"items": [{"id": 3}], "total_count": 3, "has_more": False}]
def fetch(page):
calls.append(page)
return pages[page - 1]
self.assertEqual(mod.collect_pages(fetch, "id", page_size=2), [{"id": 1}, {"id": 2}, {"id": 3}])
self.assertEqual(calls, [1, 2])
with self.assertRaises(mod.ValidationError):
mod.collect_pages(lambda _: {"items": [{"id": 1}], "total_count": 3, "has_more": True}, "id", page_size=2)
def test_pagination_validates_all_declared_metadata_and_identity_types(self):
bad_pages = [
{"items": [{"id": 1}], "total_count": 0, "has_more": True},
{"items": [{"id": 1}, {"id": 2}], "total_count": 2, "has_more": True},
{"items": [{"id": []}], "has_more": False},
{"items": [{"id": 1.5}], "has_more": False},
{"items": [{"id": 0}], "has_more": False},
{"items": [{"id": True}], "has_more": False},
{"items": [{"id": 1}, {"id": 1}], "has_more": False},
{"items": None, "has_more": False},
{"items": [], "has_more": True},
]
for page in bad_pages:
with self.subTest(page=bad_pages.index(page)), self.assertRaises(mod.ValidationError):
mod.collect_pages(lambda _: page, "id", page_size=2, max_pages=2)
pages = [{"items": [{"id": 1}], "total_count": 2, "has_more": True},
{"items": [{"id": 2}], "total_count": 3, "has_more": False}]
with self.assertRaises(mod.ValidationError):
mod.collect_pages(lambda i: pages[i - 1], "id", page_size=1)
self.assertEqual(mod.collect_pages(lambda _: {"items": None, "total_count": 0, "has_more": False}, "id"), [])
def test_cli_errors_are_sanitized_without_tracebacks_or_echoed_values(self):
env = dict(os.environ, PYTHONDONTWRITEBYTECODE="1")
base = [sys.executable, str(MODPATH)]
for args in (["unknown-secret-value"], ["check-cache", "--issue", "secret-value"],
["check-unchanged", "--before", "secret-value", "--after", "missing"]):
p = subprocess.run(base + args, capture_output=True, text=True, env=env)
self.assertNotEqual(p.returncode, 0)
self.assertNotIn("secret-value", p.stderr + p.stdout)
self.assertNotIn("Traceback", p.stderr + p.stdout)
self.assertEqual(json.loads(p.stderr), {"error": "invalid input"})
with tempfile.TemporaryDirectory(dir="/opt/data/cache/scratch") as td:
path = Path(td) / "pages.json"
path.write_text(json.dumps([{ "items": [{"id": 1}], "has_more": True, "total_count": 2 }]))
p = subprocess.run(base + ["check-pages", "--pages", str(path), "--key", "id"],
capture_output=True, text=True, env=env)
self.assertNotEqual(p.returncode, 0)
self.assertNotIn("Traceback", p.stderr)
self.assertEqual(json.loads(p.stderr), {"error": "invalid input"})
def test_target_identifiers_preserve_dots_but_reject_marker_breaking_whitespace(self):
r = result(); paths = {"src/a.py", "tests/test_a.py"}
r["target"].update(owner="acme.name", repository="repo.name")
body = mod.render_review(r, paths, declaration={"mode": "none"})
self.assertEqual(mod.parse_review(body, paths), r)
r["target"]["owner"] = "acme name"
with self.assertRaises(mod.ValidationError):
mod.validate_result(r, paths)
def test_rendered_paths_cannot_inject_markdown_html_or_mentions(self):
r = result(); path = "src/<script>@user`[link].py"
r["criteria"][0]["implementation_paths"] = [path]
paths = {path, "tests/test_a.py"}
body = mod.render_review(r, paths, declaration={"mode": "none"})
self.assertNotIn("<script>", body)
self.assertNotIn("@user", body)
self.assertNotIn("`[link]", body)
self.assertEqual(mod.parse_review(body, paths), r)
def test_strict_types_limits_and_duplicate_nested_keys(self):
paths = {"src/a.py", "tests/test_a.py"}
for bad_json in ('{"outer":{"x":1,"x":2}}', '{"x":Infinity}', '{"x":-Infinity}'):
with self.subTest(bad_json=bad_json), self.assertRaises(mod.ValidationError):
mod.loads_json(bad_json)
cases = []
r = result(); r["target"]["pull"] = False; cases.append(r)
r = result(); r["criteria"].append(copy.deepcopy(r["criteria"][0])); cases.append(r)
r = result(); r["criteria"][0]["contract"] = "a" * 4001; cases.append(r)
r = result(); r["criteria"][0]["criterion"] = "a" * 201; cases.append(r)
r = result(); r["criteria"][0]["result"] = {}; cases.append(r)
r = result(); r["criteria"][0]["test_paths"] *= 2; cases.append(r)
r = result(); r["criteria"][0]["test_paths"] = ["/absolute"]; cases.append(r)
r = result(); r["criteria"][0]["test_paths"] = ["tests/../file.py"]; cases.append(r)
r = result(); r["extra"] = None; cases.append(r)
r = result(); del r["scope"]; cases.append(r)
for value in cases:
with self.subTest(case=cases.index(value)), self.assertRaises(mod.ValidationError):
mod.validate_result(value, paths)
def test_cli_verify_publication_and_rejects_raw_crlf_body(self):
before, after, review, r, paths = publication_fixture()
env = dict(os.environ, PYTHONDONTWRITEBYTECODE="1")
with tempfile.TemporaryDirectory(dir="/opt/data/cache/scratch") as td:
root = Path(td)
for name, value in (("before", before), ("after", after), ("exact", review),
("result", r), ("paths", sorted(paths)), ("inline", [])):
(root / (name + ".json")).write_text(json.dumps(value, ensure_ascii=False))
cmd = [sys.executable, str(MODPATH), "verify-publication", "--before", str(root / "before.json"),
"--after", str(root / "after.json"), "--exact-review", str(root / "exact.json"),
"--result", str(root / "result.json"), "--allowed-paths", str(root / "paths.json"),
"--inline-comments", str(root / "inline.json"), "--reviewer-id", "42",
"--expected-pr-url", "https://example.test/acme/repo/pulls/7"]
p = subprocess.run(cmd, capture_output=True, text=True, env=env)
self.assertEqual(p.returncode, 0, p.stderr)
self.assertEqual(json.loads(p.stdout)["review_id"], 11)
body_path = root / "body.md"; body_path.write_bytes(review["body"].replace("\n", "\r\n").encode())
p = subprocess.run([sys.executable, str(MODPATH), "parse", "--body", str(body_path),
"--allowed-paths", str(root / "paths.json")], capture_output=True, text=True, env=env)
self.assertNotEqual(p.returncode, 0)
self.assertEqual(json.loads(p.stderr), {"error": "invalid input"})
def test_publication_accepts_causal_event_timestamp_without_forcing_clock_equality(self):
before, after, review, r, paths = publication_fixture()
event = after["timeline"][0]
event["created_at"] = event["updated_at"] = "2026-10-03T01:24:01Z"
after["pr"]["updated_at"] = event["created_at"]
verify_fixture(before, after, review, r, paths)
def test_publication_requires_independent_canonical_pr_url_anchor(self):
before, after, review, r, paths = publication_fixture()
for foreign in ("https://evil.test/acme/repo/pulls/7", "https://example.test/prefix/acme/repo/pulls/7"):
b = copy.deepcopy(before); a = copy.deepcopy(after)
b["pr"]["html_url"] = b["pr"]["url"] = foreign
a["pr"]["html_url"] = a["pr"]["url"] = foreign
a["reviews"][0]["pull_request_url"] = foreign
a["timeline"][0]["pull_request_url"] = foreign
with self.subTest(url=foreign), self.assertRaises(mod.ValidationError):
verify_fixture(b, a, a["reviews"][0], r, paths)
if __name__ == "__main__":
unittest.main()
@@ -1,250 +0,0 @@
"""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()