580 lines
36 KiB
Python
580 lines
36 KiB
Python
import importlib.util
|
|
import os
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
SCRIPT = Path(__file__).parents[2] / 'agents/impl/skills/gitea-issue-rework/scripts/rework_input_gate.py'
|
|
|
|
class InputGateTests(unittest.TestCase):
|
|
def load(self):
|
|
spec = importlib.util.spec_from_file_location('rework_input_gate', SCRIPT)
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
def test_valid_input_schema_is_accepted(self):
|
|
gate = self.load()
|
|
value = {'schema':'gitea-agent.rework-input/v1',
|
|
'target':{'owner':'acme','repository':'demo','issue':7,'pr':9},
|
|
'head':'a'*40, 'contract_sha256':'b'*64}
|
|
self.assertEqual(gate.validate_input(value), value)
|
|
|
|
def test_initial_output_is_immutable_and_rejected_before_credential_or_get(self):
|
|
cli_path = SCRIPT.with_name('rework_readback_cli.py')
|
|
spec = importlib.util.spec_from_file_location('rework_readback_cli', cli_path)
|
|
cli = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(cli)
|
|
with tempfile.TemporaryDirectory() as root:
|
|
output = Path(root) / 'decision.json'
|
|
output.write_bytes(b'old\n')
|
|
output.chmod(0o600)
|
|
mode = output.stat().st_mode
|
|
with self.assertRaisesRegex(ValueError, 'output already exists'):
|
|
cli.reject_existing_output(str(output))
|
|
self.assertEqual(output.read_bytes(), b'old\n')
|
|
self.assertEqual(output.stat().st_mode, mode)
|
|
|
|
class OrderedDecisionTests(unittest.TestCase):
|
|
def setUp(self):
|
|
import copy, hashlib
|
|
from harness.tests.impl import test_rework_readback as rb
|
|
from harness.tests.spec import test_implementation_spec_review_support as spec_tests
|
|
self.rb = rb.ReworkReadbackTests(); self.rb.setUp(); self.addCleanup(self.rb.doCleanups)
|
|
self.rb.repo['default_branch'] = 'main'
|
|
self.rb.source['body'] = '<!-- gitea-agent:discovery-draft/v2 -->\nSynthetic approved source contract.\n'
|
|
self.inp = {'schema':'gitea-agent.rework-input/v1', 'target':{'owner':'acme','repository':'demo','issue':7,'pr':9},
|
|
'head':'b'*40, 'contract_sha256':hashlib.sha256(self.rb.source['body'].encode()).hexdigest()}
|
|
self.rows=[]
|
|
for role, rid, uid in [('gitea-spec-agent',301,33), ('gitea-review-agent',302,34)]:
|
|
row = copy.deepcopy(self.rb.reviews[0]); row.update(id=rid, commit_id=self.inp['head'], state='APPROVED',
|
|
user={'id':uid,'login':role}, stale=False, dismissed=False,
|
|
html_url=self.rb.pr['html_url']+'#pullreview-'+str(rid))
|
|
if role == 'gitea-spec-agent':
|
|
result = spec_tests.result(); result['target'] = dict(self.inp['target']); result['target']['pull'] = result['target'].pop('pr'); result['target']['head']=self.inp['head']; result['contract_sha256']=self.inp['contract_sha256']
|
|
row['body']=spec_tests.mod.render_review(result, {'src/a.py','tests/test_a.py'}, declaration={'mode':'none'})
|
|
else:
|
|
row['body']=self.code_body('approve')
|
|
self.rows.append(row)
|
|
self.set_rows(self.rows)
|
|
original = self.rb.request
|
|
def request(path):
|
|
from urllib.parse import urlsplit
|
|
import re
|
|
if re.fullmatch(r'/repos/acme/demo/pulls/9/reviews/[1-9][0-9]*/comments', urlsplit(path).path):
|
|
self.rb.calls.append(path); return [], {'X-Total-Count':'0'}, 200
|
|
return original(path)
|
|
self.rb.request=request
|
|
|
|
def code_body(self, verdict):
|
|
return '<!-- gitea-agent:code-review/v1 owner=acme repository=demo issue=7 pr=9 head='+self.inp['head']+' contract-sha256='+self.inp['contract_sha256']+' -->\n- Verdict: `'+verdict+'`\nBounded native code-review fixture.\n'
|
|
|
|
def set_rows(self, rows):
|
|
import copy
|
|
self.rb.reviews=copy.deepcopy(rows)
|
|
self.rb.pr['comments']=self.rb.underlying['comments']=self.rb.pr['review_comments']=len(rows)
|
|
|
|
def snapshot(self):
|
|
return self.rb.adapter().input_snapshot(self.inp)
|
|
|
|
def test_real_spec_renderer_approval_is_accepted(self):
|
|
receipt=self.snapshot()
|
|
self.assertEqual(receipt['status'],'no_op',receipt['reason_codes'])
|
|
|
|
def test_fresh_source_contract_digest_is_enforced(self):
|
|
self.assertEqual(self.snapshot()['status'], 'no_op')
|
|
self.rb.source['body'] += 'Unexpected contract mutation.\n'
|
|
with self.assertRaisesRegex(ValueError, 'contract|digest'):
|
|
self.snapshot()
|
|
|
|
def test_native_state_marker_contradiction_is_blocked(self):
|
|
import copy
|
|
rows = copy.deepcopy(self.rows)
|
|
code = next(row for row in rows if row['user']['login'] == 'gitea-review-agent')
|
|
code['body'] = self.code_body('request_changes')
|
|
code['state'] = 'APPROVED'
|
|
self.set_rows(rows)
|
|
receipt = self.snapshot()
|
|
self.assertEqual(receipt['status'], 'blocked')
|
|
self.assertIn('state_marker_contradiction', receipt['reason_codes'])
|
|
|
|
def test_legacy_output_budget_is_unchanged(self):
|
|
cli_path = SCRIPT.with_name('rework_readback_cli.py')
|
|
spec = importlib.util.spec_from_file_location('legacy_budget_probe', cli_path)
|
|
cli = importlib.util.module_from_spec(spec); spec.loader.exec_module(cli)
|
|
with tempfile.TemporaryDirectory() as td:
|
|
output=Path(td)/'legacy.json'
|
|
try:
|
|
with cli.OutputFile(output) as writer: writer.write({'data':'x'*1_000_001})
|
|
except ValueError as exc:
|
|
self.fail('legacy output below original 16MB budget must remain accepted: '+str(exc))
|
|
self.assertGreater(output.stat().st_size, 1_000_000)
|
|
|
|
def test_mixed_roles_permutations_require_rework_without_raw_mutation(self):
|
|
import copy
|
|
rows=copy.deepcopy(self.rows); rows[1].update(state='REQUEST_CHANGES',body=self.code_body('request_changes'))
|
|
for ordered in (rows, list(reversed(rows))):
|
|
self.set_rows(ordered); before=copy.deepcopy(self.rb.reviews)
|
|
receipt=self.snapshot()
|
|
self.assertEqual(receipt['status'],'rework',receipt['reason_codes'])
|
|
self.assertEqual(receipt['decision']['selected_review_ids'], {'gitea-spec-agent':301,'gitea-review-agent':302})
|
|
self.assertEqual(self.rb.reviews,before)
|
|
self.assertEqual(receipt['raw_input_snapshot']['reviews']['rows'], before)
|
|
self.assertEqual([x['id'] for x in receipt['decision']['timeline']], [301,302])
|
|
|
|
def test_only_same_role_later_approval_supersedes_request(self):
|
|
import copy
|
|
rows=copy.deepcopy(self.rows); rows[1].update(state='REQUEST_CHANGES',body=self.code_body('request_changes'))
|
|
self.set_rows(rows); self.assertEqual(self.snapshot()['status'],'rework')
|
|
later=copy.deepcopy(self.rows[1]);later.update(id=303,submitted_at='2026-01-02T00:00:00Z',updated_at='2026-01-02T00:00:00Z',html_url=self.rb.pr['html_url']+'#pullreview-303')
|
|
self.set_rows([later,*rows]); receipt=self.snapshot()
|
|
self.assertEqual(receipt['status'],'no_op',receipt['reason_codes'])
|
|
self.assertEqual(receipt['decision']['selected_review_ids']['gitea-review-agent'],303)
|
|
|
|
def test_missing_stale_and_dismissed_approvals_are_not_noop_eligible(self):
|
|
import copy
|
|
self.assertEqual(self.snapshot()['status'],'no_op')
|
|
for rows in ([self.rows[0]], [self.rows[1]], []):
|
|
self.set_rows(rows); self.assertEqual(self.snapshot()['status'],'blocked')
|
|
for field in ('stale','dismissed'):
|
|
rows=copy.deepcopy(self.rows);rows[1][field]=True;self.set_rows(rows)
|
|
self.assertEqual(self.snapshot()['status'],'blocked')
|
|
|
|
def test_current_pending_and_manual_comments_never_allow_noop(self):
|
|
import copy
|
|
self.assertEqual(self.snapshot()['status'],'no_op')
|
|
for state in ('PENDING','COMMENT'):
|
|
rows=copy.deepcopy(self.rows);rows[1]['state']=state;self.set_rows(rows)
|
|
self.assertEqual(self.snapshot()['status'],'blocked')
|
|
|
|
def test_blocked_spec_comment_dominates_code_request_changes(self):
|
|
import copy
|
|
from harness.tests.spec import test_implementation_spec_review_support as spec_tests
|
|
result=spec_tests.result('blocked_spec');result['target']={'owner':'acme','repository':'demo','issue':7,'pull':9,'head':self.inp['head']};result['contract_sha256']=self.inp['contract_sha256'];result['criteria'][0]['result']='indeterminate';result['scope']['spec_conflicts']=['src/a.py']
|
|
rows=copy.deepcopy(self.rows);rows[0].update(state='COMMENT',body=spec_tests.mod.render_review(result,{'src/a.py','tests/test_a.py'},declaration={'mode':'none'}));rows[1].update(state='REQUEST_CHANGES',body=self.code_body('request_changes'))
|
|
self.set_rows(rows);receipt=self.snapshot();self.assertEqual(receipt['status'],'blocked')
|
|
self.assertIn('blocked_spec',receipt['reason_codes'])
|
|
|
|
def test_marker_contract_and_target_defects_block_rework(self):
|
|
import copy
|
|
self.assertEqual(self.snapshot()['status'],'no_op')
|
|
for body in ('missing marker',self.code_body('approve').replace('issue=7','issue=8'),self.code_body('approve').replace(self.inp['contract_sha256'],'f'*64)):
|
|
rows=copy.deepcopy(self.rows);rows[1]['body']=body;self.set_rows(rows)
|
|
self.assertEqual(self.snapshot()['status'],'blocked')
|
|
|
|
def test_unknown_role_cannot_replace_authoritative_code_review(self):
|
|
import copy
|
|
self.assertEqual(self.snapshot()['status'],'no_op')
|
|
unknown=copy.deepcopy(self.rows[1]);unknown['user']={'id':99,'login':'reader'}
|
|
self.set_rows([self.rows[0],unknown]);receipt=self.snapshot()
|
|
self.assertEqual(receipt['status'],'blocked');self.assertIn('unknown_role',receipt['decision']['timeline'][1]['reason_codes'])
|
|
|
|
def test_native_rows_source_marker_and_default_branch_fail_closed(self):
|
|
import copy
|
|
self.assertEqual(self.snapshot()['status'],'no_op')
|
|
for field,bad in [('id',True),('state','UNRECOGNIZED'),('stale','false'),('dismissed',None),('submitted_at','not-a-timestamp')]:
|
|
rows=copy.deepcopy(self.rows);rows[1][field]=bad;self.set_rows(rows)
|
|
with self.subTest(field=field),self.assertRaises(ValueError):self.snapshot()
|
|
self.set_rows(self.rows)
|
|
original=self.rb.source['body'];self.rb.source['body']=original.replace(' -->',' --> suffix',1)
|
|
import hashlib
|
|
self.inp['contract_sha256']=hashlib.sha256(self.rb.source['body'].encode()).hexdigest()
|
|
with self.assertRaisesRegex(ValueError,'discovery'):self.snapshot()
|
|
self.rb.source['body']=original;self.inp['contract_sha256']=hashlib.sha256(original.encode()).hexdigest();self.rb.pr['base']['ref']='other'
|
|
with self.assertRaisesRegex(ValueError,'default branch'):self.snapshot()
|
|
|
|
def test_duplicate_review_ids_and_capture_drift_fail_closed(self):
|
|
import copy
|
|
self.assertEqual(self.snapshot()['status'],'no_op')
|
|
self.set_rows([*self.rows,copy.deepcopy(self.rows[1])])
|
|
with self.assertRaisesRegex(ValueError,'duplicate'):self.snapshot()
|
|
self.set_rows(self.rows);old=self.rb.request;users=[0]
|
|
def request(path):
|
|
if path=='/user':
|
|
users[0]+=1
|
|
if users[0]==2:self.rb.source['title']='Changed during second capture'
|
|
return old(path)
|
|
self.rb.request=request
|
|
with self.assertRaisesRegex(ValueError,'concurrent provider drift'):self.snapshot()
|
|
self.assertEqual(users[0],2)
|
|
|
|
def test_timezone_and_nanosecond_order_is_exact(self):
|
|
import copy
|
|
rows=copy.deepcopy(self.rows);rows[1].update(state='REQUEST_CHANGES',body=self.code_body('request_changes'),submitted_at='2026-01-01T00:00:00.000000001Z',updated_at='2026-01-01T00:00:00.000000001Z')
|
|
later=copy.deepcopy(self.rows[1]);later.update(id=303,submitted_at='2026-01-01T01:00:00.000000002+01:00',updated_at='2026-01-01T01:00:00.000000002+01:00',html_url=self.rb.pr['html_url']+'#pullreview-303')
|
|
self.set_rows([later,*rows]);receipt=self.snapshot();self.assertEqual(receipt['status'],'no_op');self.assertEqual(receipt['decision']['selected_review_ids']['gitea-review-agent'],303)
|
|
|
|
def test_real_public_cli_runs_gate_without_selected_review(self):
|
|
import hashlib,json,sys
|
|
from unittest.mock import patch as mock
|
|
def load(name,path):
|
|
spec=importlib.util.spec_from_file_location(name,path);mod=importlib.util.module_from_spec(spec);spec.loader.exec_module(mod);return mod
|
|
cli=load('gate_cli_test',SCRIPT.with_name('rework_readback_cli.py'));command=load('gate_command_test',SCRIPT.with_name('rework_comment.py'))
|
|
with tempfile.TemporaryDirectory() as td:
|
|
root=Path(td);target=root/'target.json';provider=root/'provider.json';output=root/'decision.json'
|
|
target.write_text(json.dumps(self.inp));provider.write_text(json.dumps(self.rb.contract));target.chmod(0o600);provider.chmod(0o600)
|
|
args=['rework_comment.py','input-gate','--target',str(target),'--target-sha256',hashlib.sha256(target.read_bytes()).hexdigest(),'--provider-contract',str(provider),'--provider-contract-sha256',hashlib.sha256(provider.read_bytes()).hexdigest(),'--actor-id','42','--output',str(output)]
|
|
with mock.dict(sys.modules,{'rework_readback_cli':cli}),mock.object(sys,'argv',args),mock.dict(os.environ,{'HERMES_HOME':td}),mock.object(cli,'load_role_token',return_value='PUBLIC'+'-fixture-value') as loader,mock.object(cli,'RestTransport',return_value=self.rb.request):
|
|
self.assertEqual(command.main(),0);self.assertTrue(loader.called)
|
|
self.assertEqual(json.loads(output.read_text())['status'],'no_op');self.assertEqual(output.stat().st_mode&0o777,0o600)
|
|
import copy
|
|
rows=copy.deepcopy(self.rows);rows[1].update(state='REQUEST_CHANGES',body=self.code_body('request_changes'))
|
|
self.set_rows(list(reversed(rows)));output=root/'rework-decision.json';args[-1]=str(output)
|
|
with mock.dict(sys.modules,{'rework_readback_cli':cli}),mock.object(sys,'argv',args),mock.dict(os.environ,{'HERMES_HOME':td}),mock.object(cli,'load_role_token',return_value='PUBLIC'+'-fixture-value'),mock.object(cli,'RestTransport',return_value=self.rb.request):
|
|
self.assertEqual(command.main(),0)
|
|
actual=json.loads(output.read_text());self.assertEqual(actual['status'],'rework');self.assertEqual(actual['decision']['rework_review_ids'],[302])
|
|
saved=output.read_bytes()
|
|
with mock.dict(sys.modules,{'rework_readback_cli':cli}),mock.object(sys,'argv',args),mock.object(cli,'load_role_token') as loader,mock.object(cli,'RestTransport') as transport:
|
|
self.assertEqual(command.main(),2);loader.assert_not_called();transport.assert_not_called()
|
|
self.assertEqual(output.read_bytes(),saved)
|
|
|
|
def test_initial_output_atomic_publish_preserves_race_created_file(self):
|
|
from unittest.mock import patch as mock
|
|
spec=importlib.util.spec_from_file_location('no_clobber_test',SCRIPT.with_name('rework_readback_cli.py'));cli=importlib.util.module_from_spec(spec);spec.loader.exec_module(cli)
|
|
with tempfile.TemporaryDirectory() as td:
|
|
output=Path(td)/'decision.json';original_link=cli.os.link
|
|
with cli.OutputFile(output,initial_only=True) as writer:
|
|
def race_link(*args,**kwargs):
|
|
output.write_bytes(b'raced output');output.chmod(0o600);return original_link(*args,**kwargs)
|
|
with mock.object(cli.os,'link',side_effect=race_link),self.assertRaisesRegex(ValueError,'created during capture'):writer.write({'decision':'bounded'})
|
|
self.assertEqual(output.read_bytes(),b'raced output');self.assertEqual(output.stat().st_mode&0o777,0o600)
|
|
self.assertEqual([x.name for x in Path(td).iterdir()],['decision.json'])
|
|
|
|
def test_initial_output_total_byte_budget_rejects_without_artifact(self):
|
|
spec=importlib.util.spec_from_file_location('size_gate_test',SCRIPT.with_name('rework_readback_cli.py'));cli=importlib.util.module_from_spec(spec);spec.loader.exec_module(cli)
|
|
with tempfile.TemporaryDirectory() as td:
|
|
output=Path(td)/'decision.json'
|
|
with cli.OutputFile(output,initial_only=True) as writer,self.assertRaisesRegex(ValueError,'output size'):writer.write({'raw':'x'*1_000_001})
|
|
self.assertFalse(output.exists())
|
|
|
|
def test_old_head_state_contradiction_is_history_only(self):
|
|
import copy
|
|
self.assertEqual(self.snapshot()['status'],'no_op')
|
|
old=copy.deepcopy(self.rows[1]);old.update(id=303,commit_id='a'*40,
|
|
body=self.code_body('request_changes').replace(self.inp['head'],'a'*40),
|
|
html_url=self.rb.pr['html_url']+'#pullreview-303')
|
|
self.set_rows([old,*self.rows]);receipt=self.snapshot()
|
|
self.assertEqual(receipt['status'],'no_op',receipt['reason_codes'])
|
|
history=next(x for x in receipt['decision']['timeline'] if x['id']==303)
|
|
self.assertIn('old_head',history['reason_codes']);self.assertFalse(history['eligible'])
|
|
|
|
def test_same_instant_body_conflict_has_no_selected_review(self):
|
|
import copy
|
|
self.assertEqual(self.snapshot()['status'],'no_op')
|
|
other=copy.deepcopy(self.rows[1]);other.update(id=303,body=other['body']+'Different finding text.\n',html_url=self.rb.pr['html_url']+'#pullreview-303')
|
|
self.set_rows([other,*self.rows]);receipt=self.snapshot()
|
|
self.assertEqual(receipt['status'],'blocked');self.assertIn('conflicting_same_time_reviews',receipt['reason_codes'])
|
|
self.assertIsNone(receipt['decision']['selected_review_ids']['gitea-review-agent'])
|
|
|
|
def test_ambiguous_code_marker_or_verdict_lines_block(self):
|
|
import copy
|
|
self.assertEqual(self.snapshot()['status'],'no_op')
|
|
for suffix in ('- Verdict: malformed\n','Quoted marker: '+self.code_body('approve').splitlines()[0]+'\n'):
|
|
rows=copy.deepcopy(self.rows);rows[1]['body']+=suffix;self.set_rows(rows)
|
|
with self.subTest(suffix=suffix):self.assertEqual(self.snapshot()['status'],'blocked')
|
|
|
|
def test_invalid_timezone_minute_is_rejected(self):
|
|
import copy
|
|
self.assertEqual(self.snapshot()['status'],'no_op')
|
|
rows=copy.deepcopy(self.rows);rows[1]['submitted_at']='2026-01-01T00:00:00+00:99';self.set_rows(rows)
|
|
with self.assertRaisesRegex(ValueError,'timestamp|submitted_at'):self.snapshot()
|
|
|
|
def test_rework_targets_exclude_spec_approvals(self):
|
|
import copy
|
|
self.assertEqual(self.snapshot()['status'],'no_op')
|
|
rows=copy.deepcopy(self.rows);rows[1].update(state='REQUEST_CHANGES',body=self.code_body('request_changes'))
|
|
self.set_rows(rows);receipt=self.snapshot();self.assertEqual(receipt['status'],'rework')
|
|
self.assertEqual(receipt['decision'].get('rework_review_ids'),[302])
|
|
|
|
def test_cli_preflight_rejects_bool_target_before_credentials(self):
|
|
import hashlib,json,sys,copy
|
|
from unittest.mock import patch as mock
|
|
spec=importlib.util.spec_from_file_location('cli_preflight_test',SCRIPT.with_name('rework_readback_cli.py'));cli=importlib.util.module_from_spec(spec);spec.loader.exec_module(cli)
|
|
spec=importlib.util.spec_from_file_location('cmd_preflight_test',SCRIPT.with_name('rework_comment.py'));cmd=importlib.util.module_from_spec(spec);spec.loader.exec_module(cmd)
|
|
with tempfile.TemporaryDirectory() as td:
|
|
root=Path(td);target=root/'target.json';provider=root/'provider.json';output=root/'decision.json'
|
|
bad=copy.deepcopy(self.inp);bad['target']['issue']=True;target.write_text(json.dumps(bad));provider.write_text(json.dumps(self.rb.contract));target.chmod(0o600);provider.chmod(0o600)
|
|
args=['command','input-gate','--target',str(target),'--target-sha256',hashlib.sha256(target.read_bytes()).hexdigest(),'--provider-contract',str(provider),'--provider-contract-sha256',hashlib.sha256(provider.read_bytes()).hexdigest(),'--actor-id','42','--output',str(output)]
|
|
with mock.dict(sys.modules,{'rework_readback_cli':cli}),mock.object(sys,'argv',args),mock.object(cli,'load_role_token') as loader,mock.object(cli,'RestTransport') as transport:
|
|
self.assertEqual(cmd.main(),2);loader.assert_not_called();transport.assert_not_called()
|
|
self.assertFalse(output.exists())
|
|
|
|
def test_cli_capture_race_preserves_foreign_output(self):
|
|
import hashlib,json,sys
|
|
from unittest.mock import patch as mock
|
|
spec=importlib.util.spec_from_file_location('cli_race_test',SCRIPT.with_name('rework_readback_cli.py'));cli=importlib.util.module_from_spec(spec);spec.loader.exec_module(cli)
|
|
spec=importlib.util.spec_from_file_location('cmd_race_test',SCRIPT.with_name('rework_comment.py'));cmd=importlib.util.module_from_spec(spec);spec.loader.exec_module(cmd)
|
|
with tempfile.TemporaryDirectory() as td:
|
|
root=Path(td);target=root/'target.json';provider=root/'provider.json';output=root/'decision.json'
|
|
target.write_text(json.dumps(self.inp));provider.write_text(json.dumps(self.rb.contract));target.chmod(0o600);provider.chmod(0o600)
|
|
args=['command','input-gate','--target',str(target),'--target-sha256',hashlib.sha256(target.read_bytes()).hexdigest(),'--provider-contract',str(provider),'--provider-contract-sha256',hashlib.sha256(provider.read_bytes()).hexdigest(),'--actor-id','42','--output',str(output)]
|
|
requests=[]
|
|
def transport(path):
|
|
requests.append(path)
|
|
if len(requests)==1:output.write_bytes(b'created concurrently');output.chmod(0o600)
|
|
return self.rb.request(path)
|
|
with mock.dict(sys.modules,{'rework_readback_cli':cli}),mock.object(sys,'argv',args),mock.dict(os.environ,{'HERMES_HOME':td}),mock.object(cli,'load_role_token',return_value='PUBLIC'+'-fixture-value'),mock.object(cli,'RestTransport',return_value=transport):
|
|
self.assertEqual(cmd.main(),2)
|
|
self.assertGreater(len(requests),1);self.assertEqual(output.read_bytes(),b'created concurrently');self.assertEqual(output.stat().st_mode&0o777,0o600)
|
|
|
|
|
|
def test_pr_comment_counter_mismatch_preserves_no_op_and_diagnostics(self):
|
|
self.assertEqual(self.snapshot()['status'], 'no_op')
|
|
self.rb.pr['comments'] += 4
|
|
receipt = self.snapshot()
|
|
self.assertEqual(receipt['status'], 'no_op')
|
|
diagnostics = receipt['counter_diagnostics']
|
|
self.assertEqual(set(diagnostics), {'inline', 'pr', 'pr_issue'})
|
|
self.assertEqual(diagnostics['pr']['status'], 'mismatch')
|
|
|
|
def test_review_counter_mismatch_preserves_rework(self):
|
|
import copy
|
|
rows = copy.deepcopy(self.rows)
|
|
rows[1].update(state='REQUEST_CHANGES', body=self.code_body('request_changes'))
|
|
self.set_rows(rows)
|
|
self.rb.pr['review_comments'] += 3
|
|
receipt = self.snapshot()
|
|
self.assertEqual(receipt['status'], 'rework')
|
|
self.assertEqual(receipt['counter_diagnostics']['inline']['status'], 'mismatch')
|
|
self.assertEqual(receipt['decision']['rework_review_ids'], [302])
|
|
|
|
def test_malformed_pr_counters_are_unverified_diagnostics(self):
|
|
self.rb.pr['comments'] = 'not-a-count'
|
|
receipt = self.snapshot()
|
|
self.assertEqual(receipt['status'], 'no_op')
|
|
self.assertEqual(receipt['counter_diagnostics']['pr']['status'], 'unverified')
|
|
self.assertIn('field', receipt['counter_diagnostics']['pr'])
|
|
|
|
def test_unavailable_inline_endpoint_is_rejected_even_with_pr_count_fallback(self):
|
|
from urllib.parse import urlsplit
|
|
original = self.rb.request
|
|
def unavailable(path):
|
|
if '/pulls/9/reviews/' in urlsplit(path).path and urlsplit(path).path.endswith('/comments'):
|
|
self.rb.calls.append(path)
|
|
return {'message': 'not available'}, {}, 404
|
|
return original(path)
|
|
self.rb.request = unavailable
|
|
self.rb.contract['inline']['mode'] = 'per_review_or_pr_count'
|
|
with self.assertRaisesRegex(ValueError, 'inline collection unavailable'):
|
|
self.snapshot()
|
|
|
|
def test_inline_endpoint_405_is_rejected_even_with_pr_count_fallback(self):
|
|
from urllib.parse import urlsplit
|
|
original = self.rb.request
|
|
def unavailable(path):
|
|
if '/pulls/9/reviews/' in urlsplit(path).path and urlsplit(path).path.endswith('/comments'):
|
|
self.rb.calls.append(path)
|
|
return {'message': 'method not allowed'}, {}, 405
|
|
return original(path)
|
|
self.rb.request = unavailable
|
|
self.rb.contract['inline']['mode'] = 'per_review_or_pr_count'
|
|
with self.assertRaisesRegex(ValueError, 'inline collection unavailable'):
|
|
self.snapshot()
|
|
|
|
def test_publication_snapshot_remains_strict_on_pr_counter_mismatch(self):
|
|
from harness.tests.impl.test_rework_readback import ReworkReadbackTests
|
|
publication = ReworkReadbackTests()
|
|
publication.setUp()
|
|
self.addCleanup(publication.doCleanups)
|
|
publication.adapter().snapshot(publication.expected)
|
|
publication.pr['comments'] += 1
|
|
with self.assertRaisesRegex(ValueError, 'provider aggregate count mismatch'):
|
|
publication.adapter().snapshot(publication.expected)
|
|
|
|
def test_complete_lists_with_zero_pr_issue_counter_preserve_no_op(self):
|
|
self.rb.pr['comments'] = self.rb.underlying['comments'] = 0
|
|
receipt = self.snapshot()
|
|
self.assertEqual(receipt['status'], 'no_op')
|
|
for scope in ('pr', 'pr_issue'):
|
|
diagnostic = receipt['counter_diagnostics'][scope]
|
|
self.assertEqual(diagnostic, {'field':'comments',
|
|
'components':['top_level','reviews','inline'], 'observed':0,
|
|
'expected':2, 'status':'mismatch'})
|
|
self.assertEqual(receipt['raw_input_snapshot'][scope]['comments'], 0)
|
|
|
|
def test_missing_and_invalid_counters_do_not_replace_record_validation(self):
|
|
import copy
|
|
saved_pr, saved_issue = copy.deepcopy(self.rb.pr), copy.deepcopy(self.rb.underlying)
|
|
for scope, field in (('inline','review_comments'), ('pr','comments'), ('pr_issue','comments')):
|
|
for value in (None, True, -1, '0', 0.5, 'missing'):
|
|
with self.subTest(scope=scope, value=value):
|
|
self.rb.pr, self.rb.underlying = copy.deepcopy(saved_pr), copy.deepcopy(saved_issue)
|
|
record = self.rb.underlying if scope == 'pr_issue' else self.rb.pr
|
|
if value == 'missing': record.pop(field)
|
|
else: record[field] = value
|
|
receipt = self.snapshot()
|
|
self.assertEqual(receipt['status'], 'no_op')
|
|
self.assertEqual(receipt['counter_diagnostics'][scope]['status'], 'unverified')
|
|
raw_record = receipt['raw_input_snapshot']['pr_issue' if scope == 'pr_issue' else 'pr']
|
|
if value == 'missing': self.assertNotIn(field, raw_record)
|
|
else: self.assertEqual(raw_record[field], value)
|
|
|
|
def test_counter_mismatch_does_not_unblock_lone_approval(self):
|
|
self.set_rows(self.rows[:1])
|
|
self.rb.pr['comments'] = self.rb.underlying['comments'] = 0
|
|
receipt = self.snapshot()
|
|
self.assertEqual(receipt['status'], 'blocked')
|
|
self.assertEqual(receipt['counter_diagnostics']['pr']['status'], 'mismatch')
|
|
self.assertEqual(receipt['decision']['rework_review_ids'], [])
|
|
|
|
def spec_body(self, verdict, head=None):
|
|
from harness.tests.spec import test_implementation_spec_review_support as spec_tests
|
|
result = spec_tests.result(verdict)
|
|
result['target'] = {'owner':'acme','repository':'demo','issue':7,'pull':9,
|
|
'head':head or self.inp['head']}
|
|
result['contract_sha256'] = self.inp['contract_sha256']
|
|
if verdict == 'request_changes':
|
|
result['criteria'][0].update(result='unsatisfied', reason='Bounded regression finding.')
|
|
result['conclusion'] = 'Bounded correction required.'
|
|
return spec_tests.mod.render_review(result, {'src/a.py','tests/test_a.py'}, declaration={'mode':'none'})
|
|
|
|
def test_lone_spec_request_changes_allows_rework(self):
|
|
rows = [dict(self.rows[0], state='REQUEST_CHANGES', body=self.spec_body('request_changes'))]
|
|
self.set_rows(rows)
|
|
receipt = self.snapshot()
|
|
self.assertEqual(receipt['status'], 'rework', receipt['reason_codes'])
|
|
self.assertEqual(receipt['decision']['selected_review_ids']['gitea-spec-agent'], 301)
|
|
self.assertIsNone(receipt['decision']['selected_review_ids']['gitea-review-agent'])
|
|
self.assertEqual(receipt['decision']['rework_review_ids'], [301])
|
|
self.assertIn('missing_current_review', receipt['reason_codes'])
|
|
|
|
def test_lone_code_request_changes_allows_rework(self):
|
|
import copy
|
|
row = copy.deepcopy(self.rows[1])
|
|
row.update(state='REQUEST_CHANGES', body=self.code_body('request_changes'))
|
|
self.set_rows([row])
|
|
receipt = self.snapshot()
|
|
self.assertEqual(receipt['status'], 'rework', receipt['reason_codes'])
|
|
self.assertIsNone(receipt['decision']['selected_review_ids']['gitea-spec-agent'])
|
|
self.assertEqual(receipt['decision']['selected_review_ids']['gitea-review-agent'], 302)
|
|
self.assertEqual(receipt['decision']['rework_review_ids'], [302])
|
|
self.assertIn('missing_current_review', receipt['reason_codes'])
|
|
|
|
def test_valid_request_changes_survives_invalid_counterpart_history(self):
|
|
import copy
|
|
for request_index in (0, 1):
|
|
for excluded in ('stale', 'dismissed', 'old_head'):
|
|
with self.subTest(request_role=request_index, excluded=excluded):
|
|
rows = copy.deepcopy(self.rows)
|
|
request = rows[request_index]
|
|
request.update(state='REQUEST_CHANGES', body=self.spec_body('request_changes')
|
|
if request_index == 0 else self.code_body('request_changes'))
|
|
other = rows[1-request_index]
|
|
if excluded == 'old_head':
|
|
other.update(commit_id='a'*40, body=self.spec_body('approve', head='a'*40)
|
|
if request_index == 1 else self.code_body('approve').replace(self.inp['head'], 'a'*40))
|
|
else: other[excluded] = True
|
|
self.set_rows(rows)
|
|
receipt = self.snapshot()
|
|
self.assertEqual(receipt['status'], 'rework', receipt['reason_codes'])
|
|
self.assertEqual(receipt['decision']['rework_review_ids'], [request['id']])
|
|
self.assertIsNone(receipt['decision']['selected_review_ids'][other['user']['login']])
|
|
self.assertIn('missing_current_review', receipt['reason_codes'])
|
|
|
|
def test_excluded_lone_requests_cannot_authorize_rework(self):
|
|
import copy
|
|
for request_index in (0, 1):
|
|
for excluded in ('stale', 'dismissed', 'old_head'):
|
|
with self.subTest(request_role=request_index, excluded=excluded):
|
|
request = copy.deepcopy(self.rows[request_index])
|
|
head = 'a'*40 if excluded == 'old_head' else self.inp['head']
|
|
request.update(state='REQUEST_CHANGES', commit_id=head,
|
|
body=self.spec_body('request_changes', head=head) if request_index == 0
|
|
else self.code_body('request_changes').replace(self.inp['head'], head))
|
|
if excluded != 'old_head': request[excluded] = True
|
|
self.set_rows([request])
|
|
receipt = self.snapshot()
|
|
self.assertEqual(receipt['status'], 'blocked')
|
|
self.assertEqual(receipt['decision']['rework_review_ids'], [])
|
|
|
|
def test_superseded_request_with_missing_counterpart_stays_blocked(self):
|
|
import copy
|
|
request = copy.deepcopy(self.rows[1])
|
|
request.update(state='REQUEST_CHANGES', body=self.code_body('request_changes'))
|
|
approval = copy.deepcopy(self.rows[1])
|
|
approval.update(id=303, submitted_at='2026-01-02T00:00:00Z', updated_at='2026-01-02T00:00:00Z',
|
|
html_url=self.rb.pr['html_url'] + '#pullreview-303')
|
|
self.set_rows([request, approval])
|
|
receipt = self.snapshot()
|
|
self.assertEqual(receipt['status'], 'blocked', receipt['reason_codes'])
|
|
self.assertNotEqual(receipt['status'], 'rework')
|
|
self.assertNotEqual(receipt['status'], 'no_op')
|
|
self.assertEqual(receipt['decision']['rework_review_ids'], [])
|
|
|
|
def test_true_blockers_override_valid_request_changes(self):
|
|
import copy
|
|
request = copy.deepcopy(self.rows[1])
|
|
request.update(state='REQUEST_CHANGES', body=self.code_body('request_changes'))
|
|
for state in ('COMMENT', 'PENDING'):
|
|
with self.subTest(state=state):
|
|
rows = copy.deepcopy(self.rows)
|
|
rows[1] = copy.deepcopy(request)
|
|
rows[0]['state'] = state
|
|
self.set_rows(rows)
|
|
receipt = self.snapshot()
|
|
self.assertEqual(receipt['status'], 'blocked')
|
|
self.assertIn('comment_manual_classification' if state == 'COMMENT'
|
|
else 'pending_authoritative_review', receipt['reason_codes'])
|
|
rows = copy.deepcopy(self.rows)
|
|
rows[1] = request
|
|
rows[0]['body'] = 'missing marker'
|
|
self.set_rows(rows)
|
|
self.assertEqual(self.snapshot()['status'], 'blocked')
|
|
rows = copy.deepcopy(self.rows)
|
|
rows[1] = request
|
|
rows[0]['body'] = rows[0]['body'].replace(self.inp['contract_sha256'], 'f'*64)
|
|
self.set_rows(rows)
|
|
self.assertEqual(self.snapshot()['status'], 'blocked')
|
|
rows = copy.deepcopy(self.rows)
|
|
result = __import__('harness.tests.spec.test_implementation_spec_review_support', fromlist=['result'])
|
|
spec_result = result.result('blocked_spec')
|
|
spec_result['target'] = {'owner':'acme','repository':'demo','issue':7,'pull':9,'head':self.inp['head']}
|
|
spec_result['contract_sha256'] = self.inp['contract_sha256']
|
|
spec_result['criteria'][0]['result'] = 'indeterminate'
|
|
spec_result['scope']['spec_conflicts'] = ['src/a.py']
|
|
rows[0].update(state='COMMENT', body=result.mod.render_review(spec_result, {'src/a.py','tests/test_a.py'}, declaration={'mode':'none'}))
|
|
rows[1] = request
|
|
self.set_rows(rows)
|
|
receipt = self.snapshot()
|
|
self.assertEqual(receipt['status'], 'blocked')
|
|
self.assertIn('blocked_spec', receipt['reason_codes'])
|
|
|
|
def test_lone_request_changes_keeps_rework_despite_counter_mismatch(self):
|
|
import copy
|
|
request = copy.deepcopy(self.rows[1])
|
|
request.update(state='REQUEST_CHANGES', body=self.code_body('request_changes'))
|
|
self.set_rows([request])
|
|
self.rb.pr['comments'] += 2
|
|
receipt = self.snapshot()
|
|
self.assertEqual(receipt['status'], 'rework', receipt['reason_codes'])
|
|
self.assertEqual(receipt['decision']['rework_review_ids'], [302])
|
|
self.assertEqual(receipt['counter_diagnostics']['pr']['status'], 'mismatch')
|
|
|
|
def test_counter_mismatch_does_not_hide_per_review_inline_count_mismatch(self):
|
|
self.rb.reviews[0]['comments_count'] = 1
|
|
self.rb.pr['comments'] = self.rb.underlying['comments'] = 0
|
|
with self.assertRaisesRegex(ValueError, 'per-review inline count mismatch'):
|
|
self.snapshot()
|
|
|
|
def test_counter_mismatch_does_not_hide_duplicate_review_records(self):
|
|
import copy
|
|
self.rb.reviews.append(copy.deepcopy(self.rb.reviews[0]))
|
|
self.rb.pr['comments'] = self.rb.underlying['comments'] = 0
|
|
with self.assertRaisesRegex(ValueError, 'duplicate'):
|
|
self.snapshot()
|
|
|
|
def test_counter_mismatch_does_not_hide_incomplete_review_pagination(self):
|
|
original = self.rb.request
|
|
def truncated(path):
|
|
if '/pulls/9/reviews?' in path:
|
|
payload, headers, status = original(path)
|
|
return payload, dict(headers, **{'X-Total-Count':'3'}), status
|
|
return original(path)
|
|
self.rb.request = truncated
|
|
self.rb.pr['comments'] = self.rb.underlying['comments'] = 0
|
|
with self.assertRaisesRegex(ValueError, 'truncated collection'):
|
|
self.snapshot()
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|