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,176 @@
import copy
import json
import subprocess
import sys
import tempfile
from pathlib import Path
import unittest
from harness.tests._support import spec as fixture
class RequestedReviewerProjectionTests(unittest.TestCase):
def snapshots(self):
before, after, review, result, paths = fixture.publication_fixture()
rich_user = {
'id': 42,
'login': 'spec-agent',
'full_name': 'Spec Agent',
'metadata': {
'active': True,
'roles': ['reviewer', {'scope': 'implementation-spec'}],
},
}
review['user'] = copy.deepcopy(rich_user)
after['reviews'][0]['user'] = copy.deepcopy(rich_user)
after['timeline'][0]['user'] = copy.deepcopy(rich_user)
people = [copy.deepcopy(rich_user),
{'id': 7, 'login': 'other', 'metadata': {'active': False,
'roles': ['observer']}}]
before['pr']['requested_reviewers'] = copy.deepcopy(people)
after['pr']['requested_reviewers'] = copy.deepcopy(people)
return before, after, review, result, paths
def contract(self):
return {'version': {'version': '1.27.3'},
'requested_reviewers_presentation_order': True}
def test_existing_reviewer_permutation_requires_explicit_versioned_contract(self):
before, after, review, result, paths = self.snapshots()
fixture.verify_fixture(before, after, review, result, paths, self.contract())
after['pr']['requested_reviewers'].reverse()
try:
actual = fixture.verify_fixture(before, after, review, result, paths, self.contract())
except fixture.mod.ValidationError as exc:
self.fail('versioned presentation-only permutation was rejected: ' + str(exc))
self.assertEqual(actual['review_id'], 11)
def test_missing_false_or_unrelated_projection_flags_remain_order_strict(self):
before, after, review, result, paths = self.snapshots()
after['pr']['requested_reviewers'].reverse()
for contract in (None, {'version': {'version': '1.27.3'}},
{'version': {'version': '1.27.3'}, 'requested_reviewers_presentation_order': False},
{'version': {'version': '1.27.3'}, 'supersession_same_author_only': True}):
with self.subTest(contract=contract), self.assertRaises(fixture.mod.ValidationError):
fixture.verify_fixture(before, after, review, result, paths, contract)
def test_new_reviewer_addition_can_reorder_preserved_members(self):
before, after, review, result, paths = self.snapshots()
old = [{'id': 5, 'login': 'five'}, {'id': 7, 'login': 'seven'}]
before['pr']['requested_reviewers'] = copy.deepcopy(old)
after['pr']['requested_reviewers'] = [old[1], copy.deepcopy(review['user']), old[0]]
originals = copy.deepcopy((before, after))
self.assertEqual(fixture.verify_fixture(before, after, review, result, paths,
self.contract())['review_id'], 11)
self.assertEqual((before, after), originals)
# Without opt-in only an addition preserving previous order is allowed.
with self.assertRaises(fixture.mod.ValidationError):
fixture.verify_fixture(before, after, review, result, paths)
after['pr']['requested_reviewers'] = old + [copy.deepcopy(review['user'])]
fixture.verify_fixture(before, after, review, result, paths)
def test_opt_in_new_reviewer_must_match_exact_review_user_record(self):
before, after, review, result, paths = self.snapshots()
old = [{'id': 5, 'login': 'five'}, {'id': 7, 'login': 'seven'}]
before['pr']['requested_reviewers'] = copy.deepcopy(old)
expected = copy.deepcopy(review['user'])
for forged in (
{'id': 42},
{'id': 42, 'login': 'forged', 'metadata': expected['metadata']},
{**expected, 'extra': 'unexpected'},
{**expected, 'metadata': {'active': True, 'roles': ['reviewer', {'scope': 'other'}]}},
{'id': 42, 'login': expected['login'], 'metadata': expected['metadata'], 'name': None},
):
bad = copy.deepcopy(after)
bad['pr']['requested_reviewers'] = [old[1], forged, old[0]]
with self.subTest(forged=forged), self.assertRaises(fixture.mod.ValidationError):
fixture.verify_fixture(before, bad, review, result, paths, self.contract())
def test_removals_unknown_additions_and_all_existing_record_drift_fail(self):
before, after, review, result, paths = self.snapshots()
changes = []
for people in ([{'id': 42}], after['pr']['requested_reviewers'] + [{'id': 43}],
after['pr']['requested_reviewers'] + [copy.deepcopy(after['pr']['requested_reviewers'][0])]):
bad = copy.deepcopy(after); bad['pr']['requested_reviewers'] = people; changes.append(bad)
for field in ('login', 'metadata', 'extra'):
bad = copy.deepcopy(after); bad['pr']['requested_reviewers'][0][field] = 'changed'; changes.append(bad)
bad = copy.deepcopy(after); del bad['pr']['requested_reviewers'][0]['metadata']; changes.append(bad)
for bad in changes:
with self.subTest(case=changes.index(bad)), self.assertRaises(fixture.mod.ValidationError):
fixture.verify_fixture(before, bad, review, result, paths, self.contract())
bad_before = copy.deepcopy(before)
bad_before['pr']['requested_reviewers'].append(copy.deepcopy(bad_before['pr']['requested_reviewers'][0]))
with self.assertRaises(fixture.mod.ValidationError):
fixture.verify_fixture(bad_before, after, review, result, paths, self.contract())
def test_contract_version_and_flag_types_fail_closed(self):
before, after, review, result, paths = self.snapshots()
for value in (None, 0, 1, 'true', [], {}):
contract = self.contract(); contract['requested_reviewers_presentation_order'] = value
with self.subTest(flag=value), self.assertRaises(fixture.mod.ValidationError):
fixture.verify_fixture(before, after, review, result, paths, contract)
for version in ('1.27.4', '1.27.3+patched', None, True):
contract = self.contract(); contract['version']['version'] = version
with self.subTest(version=version), self.assertRaises(fixture.mod.ValidationError):
fixture.verify_fixture(before, after, review, result, paths, contract)
def test_invalid_id_types_and_null_collections_fail_closed(self):
before, after, review, result, paths = self.snapshots()
for value in (True, 0, -1, '42', 42.0, None):
bad = copy.deepcopy(after); bad['pr']['requested_reviewers'][0]['id'] = value
with self.subTest(id=value), self.assertRaises(fixture.mod.ValidationError):
fixture.verify_fixture(before, bad, review, result, paths, self.contract())
for side in ('before', 'after'):
b, a = copy.deepcopy(before), copy.deepcopy(after)
(b if side == 'before' else a)['pr']['requested_reviewers'] = None
with self.subTest(side=side), self.assertRaises(fixture.mod.ValidationError):
fixture.verify_fixture(b, a, review, result, paths, self.contract())
def test_other_meaningful_orders_remain_strict(self):
before, after, review, result, paths = self.snapshots()
comments = [{'id': 1, 'body': 'one'}, {'id': 2, 'body': 'two'}]
for field in ('issue_comments', 'pr_comments'):
b, a = copy.deepcopy(before), copy.deepcopy(after)
b[field] = copy.deepcopy(comments); a[field] = list(reversed(comments))
with self.subTest(field=field), self.assertRaises(fixture.mod.ValidationError):
fixture.verify_fixture(b, a, review, result, paths, self.contract())
# Historical review order must stay intact, even with reviewer opt-in.
for ident in (8, 9):
old = copy.deepcopy(review); old['id'] = ident
before['reviews'].append(old); after['reviews'].insert(0, copy.deepcopy(old))
with self.assertRaisesRegex(fixture.mod.ValidationError, 'previous review order'):
fixture.verify_fixture(before, after, review, result, paths, self.contract())
def test_cli_uses_opt_in_without_mutating_input_files(self):
before, after, review, result, paths = self.snapshots()
after['pr']['requested_reviewers'].reverse()
with tempfile.TemporaryDirectory(dir='/opt/data/cache/scratch') as td:
root = Path(td)
values = {'before': before, 'after': after, 'exact-review': review,
'result': result, 'allowed-paths': list(paths),
'inline-comments': [], 'provider-contract': self.contract()}
args = [sys.executable, '-B', str(fixture.MODPATH), 'verify-publication']
for name, value in values.items():
path = root / (name + '.json'); path.write_text(json.dumps(value))
args += ['--' + name, str(path)]
args += ['--reviewer-id', '42', '--expected-pr-url', 'https://example.test/acme/repo/pulls/7']
original = {p.name: p.read_bytes() for p in root.iterdir()}
checked = subprocess.run(args, capture_output=True, text=True, timeout=15)
self.assertEqual(checked.returncode, 0, checked.stderr)
self.assertEqual(json.loads(checked.stdout)['review_id'], 11)
self.assertEqual({p.name: p.read_bytes() for p in root.iterdir()}, original)
path = root / 'provider-contract.json'
contract = self.contract(); contract['requested_reviewers_presentation_order'] = False
path.write_text(json.dumps(contract))
denied = subprocess.run(args, capture_output=True, text=True, timeout=15)
self.assertNotEqual(denied.returncode, 0)
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'):
with self.subTest(token=token):
self.assertIn(token, text)
if __name__ == '__main__':
unittest.main()
@@ -0,0 +1,322 @@
import copy
import json
import subprocess
import sys
import tempfile
import unittest
from harness.tests._support import spec as fixture
class SourceIssueReferenceProjectionTests(unittest.TestCase):
def contract(self):
return {'version': {'version': '1.27.3'},
'source_issue_cross_reference_projection': True}
def snapshots(self):
before, after, review, result, paths = fixture.publication_fixture()
url = before['pr']['html_url']
issue_url = url.rsplit('/pulls/', 1)[0] + '/issues/3'
before['issue']['html_url'] = after['issue']['html_url'] = issue_url
pr_issue = {'id': 70, 'number': 7, 'html_url': url,
'repository': {'full_name': 'acme/repo'}, 'user': {'id': 99},
'state': 'open', 'title': 'implementation', 'body': 'Closes #3',
'comments': 2, 'updated_at': before['pr']['updated_at'],
'pull_request': {'merged': False, 'html_url': url}}
before['pr_issue'] = pr_issue
after['pr_issue'] = copy.deepcopy(pr_issue)
# Native top-level reviews change review_comments, not ordinary comments.
after['pr_issue'].update(updated_at=review['submitted_at'])
event = after['timeline'][0]
event['html_url'] = url + '#issuecomment-101'
event['issue_url'] = ''
event['assets'] = []
ref_comment = {key: copy.deepcopy(event[key]) for key in
('id', 'html_url', 'pull_request_url', 'issue_url', 'user',
'body', 'assets', 'created_at', 'updated_at')}
reference = {'id': 102, 'type': 'comment_ref', 'user': copy.deepcopy(review['user']),
'body': '', 'html_url': issue_url + '#issuecomment-102',
'issue_url': issue_url, 'pull_request_url': '',
'created_at': '2026-10-03T01:24:01Z',
'updated_at': '2026-10-03T01:24:01Z',
'ref_issue': copy.deepcopy(after['pr_issue']),
'ref_comment': ref_comment, 'ref_action': 'none', 'review_id': 0}
before['issue_timeline'] = []
after['issue_timeline'] = [reference]
after['issue']['updated_at'] = reference['created_at']
return before, after, review, result, paths
def test_exact_reference_allows_only_source_issue_timestamp(self):
before, after, review, result, paths = self.snapshots()
original = copy.deepcopy((before, after))
try:
actual = fixture.verify_fixture(before, after, review, result, paths, self.contract())
except fixture.mod.ValidationError as exc:
self.fail('exact review-bound source reference rejected: ' + str(exc))
self.assertEqual(actual['source_issue_reference_ids'], [102])
self.assertEqual((before, after), original)
after['pr']['updated_at'] = after['issue']['updated_at']
with self.assertRaisesRegex(fixture.mod.ValidationError, 'timestamp projection mismatch'):
fixture.verify_fixture(before, after, review, result, paths, self.contract())
def test_pr_issue_comments_stay_unchanged_while_review_count_increments(self):
for count in (0, 2):
before, after, review, result, paths = self.snapshots()
before['pr_issue']['comments'] = after['pr_issue']['comments'] = count
after['issue_timeline'][-1]['ref_issue'] = copy.deepcopy(after['pr_issue'])
with self.subTest(count=count):
fixture.verify_fixture(before, after, review, result, paths, self.contract())
self.assertEqual(after['pr_issue']['comments'], before['pr_issue']['comments'])
self.assertEqual(after['pr']['review_comments'], before['pr']['review_comments'] + 1)
def test_pr_issue_comment_changes_and_invalid_counts_are_rejected(self):
baseline = self.snapshots()
fixture.verify_fixture(*baseline, self.contract())
# Reject both directions, including the old erroneous +1 projection.
for value in (1, 3, 4, -1, True, '2', None):
args = copy.deepcopy(baseline)
args[1]['pr_issue']['comments'] = value
args[1]['issue_timeline'][-1]['ref_issue'] = copy.deepcopy(args[1]['pr_issue'])
with self.subTest(after=value), self.assertRaisesRegex(
fixture.mod.ValidationError, 'PR Issue count projection mismatch'):
fixture.verify_fixture(*args, self.contract())
for value in (-1, True, '2', None):
args = copy.deepcopy(baseline)
args[0]['pr_issue']['comments'] = args[1]['pr_issue']['comments'] = value
args[1]['issue_timeline'][-1]['ref_issue'] = copy.deepcopy(args[1]['pr_issue'])
with self.subTest(both=value), self.assertRaisesRegex(
fixture.mod.ValidationError, 'PR Issue count projection mismatch'):
fixture.verify_fixture(*args, self.contract())
for side in (0, 1):
args = copy.deepcopy(baseline)
del args[side]['pr_issue']['comments']
with self.subTest(missing=side), self.assertRaises(fixture.mod.ValidationError):
fixture.verify_fixture(*args, self.contract())
def test_source_reference_does_not_relax_pr_review_comment_increment(self):
baseline = self.snapshots()
fixture.verify_fixture(*baseline, self.contract())
for value in (0, 2, -1, True, '1', None):
args = copy.deepcopy(baseline)
args[1]['pr']['review_comments'] = value
with self.subTest(after=value), self.assertRaisesRegex(
fixture.mod.ValidationError, 'review count projection mismatch'):
fixture.verify_fixture(*args, self.contract())
def test_wrong_reference_identity_payload_or_timestamp_is_rejected(self):
baseline = self.snapshots()
fixture.verify_fixture(*baseline, self.contract())
mutations = [('type', 'comment'), ('user.id', 43), ('user.login', 'changed'),
('ref_issue.number', 8), ('ref_issue.repository.full_name', 'other/repo'),
('ref_issue.id', 71), ('ref_comment.id', 100), ('ref_comment.id', True),
('ref_comment.user.id', 43), ('ref_comment.body', 'other review'),
('ref_comment.created_at', '2026-10-03T01:24:01Z'),
('ref_comment.updated_at', '2026-10-03T01:24:01Z'),
('ref_comment.html_url', 'https://other.test/acme/repo/pulls/7#issuecomment-101'),
('ref_comment.pull_request_url', 'https://other.test/acme/repo/pulls/7'),
('ref_comment.assets', [{'id': 9}]), ('ref_comment.issue_url', 'wrong'),
('issue_url', 'https://example.test/acme/repo/issues/4'),
('html_url', 'https://example.test/acme/repo/issues/3#issuecomment-103'),
('pull_request_url', 'https://example.test/acme/repo/pulls/7'),
('ref_action', 'close'), ('review_id', True), ('body', 'unrelated'),
('created_at', '2026-10-03T01:23:59Z'), ('updated_at', '2026-10-03T01:24:02Z')]
for field, value in mutations:
before, after, review, result, paths = copy.deepcopy(baseline)
node = after['issue_timeline'][-1]
parts = field.split('.')
for key in parts[:-1]:
node = node[key]
node[parts[-1]] = value
with self.subTest(field=field, value=value), self.assertRaises(fixture.mod.ValidationError):
fixture.verify_fixture(before, after, review, result, paths, self.contract())
for field in ('ref_issue', 'ref_comment', 'created_at', 'updated_at', 'user'):
before, after, review, result, paths = copy.deepcopy(baseline)
del after['issue_timeline'][-1][field]
with self.subTest(missing=field), self.assertRaises(fixture.mod.ValidationError):
fixture.verify_fixture(before, after, review, result, paths, self.contract())
def test_opt_in_version_and_complete_evidence_are_required(self):
baseline = self.snapshots()
fixture.verify_fixture(*baseline, self.contract())
for contract in (None, {'version': {'version': '1.27.3'}},
{'version': {'version': '1.27.3'}, 'source_issue_cross_reference_projection': False},
{'version': {'version': '1.27.2'}, 'source_issue_cross_reference_projection': True}):
with self.subTest(contract=contract), self.assertRaises(fixture.mod.ValidationError):
fixture.verify_fixture(*baseline, contract)
for value in (1, 'true', None, [], {}):
contract = self.contract(); contract['source_issue_cross_reference_projection'] = value
with self.subTest(value=value), self.assertRaises(fixture.mod.ValidationError):
fixture.verify_fixture(*baseline, contract)
for side in (0, 1):
for field in ('issue_timeline', 'pr_issue'):
args = copy.deepcopy(baseline); del args[side][field]
with self.subTest(side=side, missing=field), self.assertRaises(fixture.mod.ValidationError):
fixture.verify_fixture(*args, self.contract())
def test_no_time_tolerance_or_other_field_relaxation(self):
baseline = self.snapshots()
fixture.verify_fixture(*baseline, self.contract())
for section, field, value in [('issue', 'updated_at', '2026-10-03T01:24:02Z'),
('issue', 'title', 'modified'), ('issue', 'body', 'new contract'),
('pr_issue', 'updated_at', '2026-10-03T01:24:01Z'),
('pr_issue', 'comments', 4), ('pr_issue', 'comments', True),
('pr_issue', 'body', 'modified'), ('pr_issue', 'id', True),
('pr_issue', 'number', 8), ('pr_issue', 'state', 'closed')]:
args = copy.deepcopy(baseline); args[1][section][field] = value
with self.subTest(section=section, field=field), self.assertRaises(fixture.mod.ValidationError):
fixture.verify_fixture(*args, self.contract())
for mode in ('missing', 'duplicate', 'competing'):
args = copy.deepcopy(baseline); events = args[1]['issue_timeline']
if mode == 'missing':
events.clear()
else:
extra = copy.deepcopy(events[-1])
if mode == 'competing':
extra.update(id=103, type='comment')
events.append(extra)
with self.subTest(mode=mode), self.assertRaises(fixture.mod.ValidationError):
fixture.verify_fixture(*args, self.contract())
def test_existing_source_records_preserve_every_field_except_bound_pr_issue_projection(self):
before, after, review, result, paths = self.snapshots()
old = {'id': 90, 'type': 'pull_ref', 'body': '',
'ref_issue': copy.deepcopy(before['pr_issue']), 'user': {'id': 99}}
before['issue_timeline'].append(old)
after['issue_timeline'].insert(0, copy.deepcopy(old))
after['issue_timeline'][0]['ref_issue'] = copy.deepcopy(after['pr_issue'])
baseline = (before, after, review, result, paths)
fixture.verify_fixture(*baseline, self.contract())
for field in ('body', 'user', 'ref_issue', 'missing', 'order'):
args = copy.deepcopy(baseline)
if field == 'missing':
args[1]['issue_timeline'].pop(0)
elif field == 'order':
args[1]['issue_timeline'].reverse()
elif field == 'ref_issue':
args[1]['issue_timeline'][0]['ref_issue']['comments'] = 5
else:
args[1]['issue_timeline'][0][field] = 'tampered'
with self.subTest(field=field), self.assertRaises(fixture.mod.ValidationError):
fixture.verify_fixture(*args, self.contract())
args = copy.deepcopy(baseline)
args[0]['issue_timeline'].append({'id': 89, 'body': 'ordinary'})
args[1]['issue_timeline'].insert(1, {'id': 89, 'body': 'ordinary'})
fixture.verify_fixture(*args, self.contract())
args[1]['issue_timeline'][1]['body'] = 'changed'
with self.assertRaises(fixture.mod.ValidationError):
fixture.verify_fixture(*args, self.contract())
def test_source_reference_timestamp_does_not_apply_to_historical_reviews(self):
before, after, review, result, paths = self.snapshots()
old = copy.deepcopy(review)
old.update(id=10, body='previous', state='REQUEST_CHANGES', commit_id='d' * 40,
updated_at=before['pr']['updated_at'])
before['reviews'] = [old]
after['reviews'].insert(0, copy.deepcopy(old))
after['reviews'][0].update(official=False, dismissed=True, updated_at=review['submitted_at'])
contract = self.contract(); contract['supersession_same_author_only'] = True
fixture.verify_fixture(before, after, review, result, paths, contract)
after['reviews'][0]['updated_at'] = after['issue']['updated_at']
with self.assertRaises(fixture.mod.ValidationError):
fixture.verify_fixture(before, after, review, result, paths, contract)
def test_cli_uses_exact_reference_and_sanitizes_invalid_input(self):
from pathlib import Path
before, after, review, result, paths = self.snapshots()
with tempfile.TemporaryDirectory(dir='/opt/data/cache/scratch') as directory:
root = Path(directory)
data = {'before': before, 'after': after, 'exact-review': review, 'result': result,
'allowed-paths': sorted(paths), 'inline-comments': [], 'provider-contract': self.contract()}
command = [sys.executable, '-B', str(fixture.MODPATH), 'verify-publication',
'--reviewer-id', '42', '--expected-pr-url', before['pr']['html_url']]
for key, value in data.items():
path = root / (key + '.json')
path.write_text(json.dumps(value), encoding='utf-8')
command.extend(['--' + key, str(path)])
actual = subprocess.run(command, capture_output=True, text=True, timeout=15)
self.assertEqual(actual.returncode, 0, actual.stderr)
self.assertEqual(json.loads(actual.stdout)['source_issue_reference_ids'], [102])
after['issue_timeline'][0]['ref_comment']['body'] = 'sensitive-value-do-not-echo'
(root / 'after.json').write_text(json.dumps(after), encoding='utf-8')
rejected = subprocess.run(command, capture_output=True, text=True, timeout=15)
self.assertEqual(rejected.returncode, 2)
self.assertEqual(json.loads(rejected.stderr), {'error': 'invalid input'})
self.assertEqual(rejected.stdout, '')
self.assertNotIn('sensitive-value-do-not-echo', rejected.stdout + rejected.stderr)
def test_collector_rejects_bad_target_or_provider_before_any_read(self):
calls = []
def read_record(endpoint):
calls.append(endpoint)
self.fail('invalid collector request reached read callback')
def fetch_page(endpoint, page):
calls.append((endpoint, page))
self.fail('invalid collector request reached page callback')
for field, value in [('issue', True), ('owner', '../other'), ('repository', 'bad/name'),
('head', 'not-a-full-sha')]:
target = copy.deepcopy(fixture.result()['target']); target[field] = value
with self.subTest(field=field), self.assertRaises(fixture.mod.ValidationError):
fixture.mod.collect_publication_snapshot(read_record, fetch_page, target,
provider_contract=self.contract())
contract = self.contract(); contract['version']['version'] = 'other'
with self.assertRaises(fixture.mod.ValidationError):
fixture.mod.collect_publication_snapshot(read_record, fetch_page, fixture.result()['target'],
provider_contract=contract)
self.assertEqual(calls, [])
def test_collector_legacy_shape_and_complete_source_page_validation(self):
before, after, review, result, paths = self.snapshots()
prefix = '/repos/acme/repo'
records = {prefix + '/issues/3': before['issue'], prefix + '/pulls/7': before['pr'],
prefix + '/issues/7': before['pr_issue']}
calls = []
def fetch_page(endpoint, page):
calls.append((endpoint, page))
return {'items': [], 'total_count': 0, 'has_more': False}
legacy = fixture.mod.collect_publication_snapshot(records.__getitem__, fetch_page, result['target'])
self.assertEqual(set(legacy), {'issue', 'pr', 'issue_comments', 'pr_comments', 'reviews', 'timeline', 'files'})
self.assertNotIn((prefix + '/issues/3/timeline', 1), calls)
for envelope in ({'items': [{'id': 1}, {'id': 1}], 'total_count': 2, 'has_more': False},
{'items': [], 'total_count': 1, 'has_more': True},
{'items': None, 'has_more': False}):
def bad_page(endpoint, page):
if endpoint == prefix + '/issues/3/timeline':
return envelope
return {'items': [], 'total_count': 0, 'has_more': False}
with self.subTest(envelope=envelope), self.assertRaises(fixture.mod.ValidationError):
fixture.mod.collect_publication_snapshot(records.__getitem__, bad_page, result['target'],
provider_contract=self.contract())
def test_collector_reads_source_timeline_and_pr_issue_through_readonly_callbacks(self):
before, after, review, result, paths = self.snapshots()
mod = fixture.mod
self.assertTrue(callable(getattr(mod, 'collect_publication_snapshot', None)),
'canonical read-only publication snapshot collector missing')
prefix = '/repos/acme/repo'
endpoints = {'issue': '/issues/3', 'pr': '/pulls/7', 'pr_issue': '/issues/7',
'issue_comments': '/issues/3/comments', 'pr_comments': '/issues/7/comments',
'reviews': '/pulls/7/reviews', 'timeline': '/issues/7/timeline',
'issue_timeline': '/issues/3/timeline', 'files': '/pulls/7/files'}
captured = []
for snapshot in (before, after):
records = {prefix + endpoints[k]: v for k, v in snapshot.items()}
calls = []
def read_record(endpoint):
calls.append(('GET', endpoint))
return copy.deepcopy(records[endpoint])
def fetch_page(endpoint, page):
calls.append(('GET-page', endpoint, page))
items = records[endpoint]
return {'items': copy.deepcopy(items), 'total_count': len(items), 'has_more': False}
out = mod.collect_publication_snapshot(read_record, fetch_page, result['target'],
provider_contract=self.contract())
self.assertEqual(out, snapshot)
self.assertIn(('GET', prefix + '/issues/7'), calls)
self.assertIn(('GET-page', prefix + '/issues/3/timeline', 1), calls)
self.assertTrue(all(call[0].startswith('GET') for call in calls))
captured.append(out)
self.assertEqual(fixture.verify_fixture(*captured, review, result, paths,
self.contract())['source_issue_reference_ids'], [102])
if __name__ == '__main__':
unittest.main()