[verified] Document empty review-comments readback without count header

This commit is contained in:
2026-10-12 07:51:32 +00:00
parent 1e842a6e43
commit 62d2262b27
2 changed files with 87 additions and 0 deletions
@@ -189,6 +189,87 @@ class NativeReviewTests(unittest.TestCase):
m.publish(p, before, api, lease, self.canonical(m, p), declaration={'mode': 'none'})
self.assertNotIn('secret transport detail', str(error.exception))
self.assertEqual(api.posts, 1)
def test_empty_review_comments_no_header_contract_is_explicit(self):
text = (ROOT / 'harness/agents/review/skills/gitea-code-review/references/native-review.md').read_text()
self.assertIn('### Empty review-comments readback without a count header', text)
section = text.split('### Empty review-comments readback without a count header', 1)[1].split('\n### ', 1)[0]
for rule in ['X-Total-Count', 'comments_count=0', 'JSON `[]`',
'declared_total=None', 'rel=next', 'blind retry']:
self.assertIn(rule, section)
def test_api_adapter_empty_comments_without_header_verifies_publication(self):
m = self.module()
p, before, after, exact = publication_fixture()
def collect(source, comments=None, headers=None):
calls = []
def request(path, payload=None):
self.assertIsNone(payload, 'readback regression must be GET-only')
calls.append(path)
direct = {'/user': p['reviewer'], '/users/gitea-spec-agent': p['spec_reviewer'],
'/repos/acme/repo': source['repository'],
'/repos/acme/repo/issues/3': source['issue'],
'/repos/acme/repo/pulls/7': source['pr'],
'/repos/acme/repo/pulls/7/reviews/10': exact}
if path in direct:
return direct[path], {}, 200
from urllib.parse import urlsplit
endpoint = urlsplit(path).path
for name, suffix in [('files', '/pulls/7/files'), ('commits', '/pulls/7/commits'),
('reviews', '/pulls/7/reviews'), ('issue_comments', '/issues/3/comments'),
('pr_comments', '/issues/7/comments'),
('statuses', '/statuses/' + p['target']['head'])]:
if endpoint == '/repos/acme/repo' + suffix:
return copy.deepcopy(source[name]), {'X-Total-Count': str(len(source[name]))}, 200
if '/reviews/' in endpoint and endpoint.endswith('/comments'):
return ([] if comments is None else comments), ({} if headers is None else headers), 200
raise AssertionError('unexpected synthetic readback endpoint')
api = m.ApiAdapter(request, origin=m.ORIGIN + '/api/v1',
credential_source='$HERMES_HOME/.env:GITEA_TOKEN')
snapshot = api.snapshot(p['target'])
self.assertEqual(calls[0], '/user')
self.assertTrue(any('/reviews/' in path and '/comments?' in path for path in calls))
return api, snapshot
_, initial = collect(before)
api, final = collect(after)
m.validate_snapshot(p, initial)
m.validate_snapshot(p, final)
authoritative = api.read_review(p['target'], exact['id'])
m.verify_publication(p, initial, final, authoritative)
for snapshot in [initial, final]:
for rid in snapshot['review_comments']:
self.assertEqual(snapshot['review_comments'][rid], [])
self.assertEqual(snapshot['pagination']['review_comments_' + rid],
[{'page': 1, 'count': 0, 'declared_total': None, 'link': None}])
# The admitted control above distinguishes intentional rejection from
# a broken fixture. Missing headers never waive the review count or
# the complete-collection and newly-created no-inline-comment checks.
for count in [1, None, True]:
with self.subTest(count=count):
drift = copy.deepcopy(final)
drift['reviews'][-1]['comments_count'] = count
with self.assertRaisesRegex(m.ValidationError, 'review comment cardinality mismatch'):
m.validate_snapshot(p, drift)
_, unexpected = collect(after, comments=[{'id': 999}])
for review in unexpected['reviews']:
review['comments_count'] = 1
m.validate_snapshot(p, unexpected)
exact_with_inline = copy.deepcopy(authoritative)
exact_with_inline['comments_count'] = 1
with self.assertRaisesRegex(m.ValidationError, 'exact review binding mismatch'):
m.verify_publication(p, initial, unexpected, exact_with_inline)
with self.assertRaisesRegex(m.ValidationError, 'truncated pages'):
collect(after, headers={'X-Total-Count': '1'})
truncated = copy.deepcopy(final)
truncated['pagination']['review_comments_' + str(exact['id'])][0]['link'] = '<next>; rel=next'
with self.assertRaisesRegex(m.ValidationError, 'truncated pagination'):
m.validate_snapshot(p, truncated)
with self.assertRaisesRegex(m.ValidationError, 'truncated pagination'):
m.check_pages([{'id': i + 1} for i in range(50)],
[{'page': 1, 'count': 50, 'declared_total': None, 'link': None}])
def test_api_adapter_checks_identity_first_and_collects_all_pages(self):
m = self.module()
self.assertTrue(callable(getattr(m, 'ApiAdapter', None)), 'REST bridge is missing')