import copy import json import subprocess import sys import tempfile import unittest from harness.tests.spec import test_implementation_spec_review_support 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) after['pr_issue'].update(comments=3, 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_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()