test(harness): reorganize regression suites and preserve contract coverage
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
"""Read-only recovery must never repeat an uncertain delivery mutation."""
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
from harness.tests._support.paths import ROOT
|
||||
|
||||
PATH = ROOT / 'harness/agents/impl/skills/gitea-issue-implementation/scripts/implementation_delivery.py'
|
||||
|
||||
|
||||
class ReadOnlyAdapter:
|
||||
def __init__(self, head):
|
||||
self.head = head
|
||||
self.calls = []
|
||||
self.proof = True
|
||||
|
||||
def push(self, head):
|
||||
raise AssertionError('reconciliation must not push')
|
||||
|
||||
def create_or_reuse_pr(self, head):
|
||||
raise AssertionError('reconciliation must not create or patch')
|
||||
|
||||
def read_remote_head(self):
|
||||
self.calls.append('branch')
|
||||
return self.head
|
||||
|
||||
def read_pr(self, number):
|
||||
self.calls.append('pr')
|
||||
return {'number': number, 'head': self.head, 'state': 'open'}
|
||||
|
||||
def verify_reconciliation(self, head, number):
|
||||
self.calls.append('complete_intent')
|
||||
return self.proof
|
||||
|
||||
|
||||
class ReconciliationTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
spec = importlib.util.spec_from_file_location('delivery_reconciliation_under_test', PATH)
|
||||
cls.module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(cls.module)
|
||||
|
||||
def test_read_only_recovery_returns_exact_state_without_repeated_mutation(self):
|
||||
adapter = ReadOnlyAdapter('a' * 40)
|
||||
result = self.module.reconcile_implementation(adapter, head=adapter.head, number=19, declaration={'mode': 'none'})
|
||||
self.assertEqual(result, {'remote_head': adapter.head, 'pr': {'number': 19, 'head': adapter.head, 'state': 'open'}})
|
||||
self.assertEqual(adapter.calls, ['branch', 'pr', 'complete_intent'])
|
||||
|
||||
def test_missing_declaration_blocks_all_reads(self):
|
||||
adapter = ReadOnlyAdapter('a' * 40)
|
||||
with self.assertRaises(self.module.AdoptionError):
|
||||
self.module.reconcile_implementation(adapter, head=adapter.head, number=19, declaration=None)
|
||||
self.assertEqual(adapter.calls, [])
|
||||
|
||||
def test_invalid_identifiers_block_all_reads(self):
|
||||
for head, number in [('bad', 19), ('a' * 40, True), ('a' * 40, 0)]:
|
||||
adapter = ReadOnlyAdapter('a' * 40)
|
||||
with self.subTest(head=head, number=number), self.assertRaises(self.module.AdoptionError):
|
||||
self.module.reconcile_implementation(adapter, head=head, number=number, declaration={'mode': 'none'})
|
||||
self.assertEqual(adapter.calls, [])
|
||||
|
||||
def test_branch_mismatch_blocks_pr_and_proof_reads(self):
|
||||
adapter = ReadOnlyAdapter('b' * 40)
|
||||
with self.assertRaises(self.module.AdoptionError):
|
||||
self.module.reconcile_implementation(adapter, head='a' * 40, number=19, declaration={'mode': 'none'})
|
||||
self.assertEqual(adapter.calls, ['branch'])
|
||||
|
||||
def test_body_snapshot_scope_or_collection_failure_cannot_complete(self):
|
||||
for proof in [False, None, 1, {}, 'yes']:
|
||||
adapter = ReadOnlyAdapter('a' * 40)
|
||||
adapter.proof = proof
|
||||
with self.subTest(proof=proof), self.assertRaises(self.module.AdoptionError):
|
||||
self.module.reconcile_implementation(adapter, head=adapter.head, number=19, declaration={'mode': 'none'})
|
||||
self.assertEqual(adapter.calls, ['branch', 'pr', 'complete_intent'])
|
||||
|
||||
def test_loopback_committed_write_with_bad_ack_recovers_read_only(self):
|
||||
import json
|
||||
import threading
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from urllib.request import Request, urlopen
|
||||
spec = importlib.util.spec_from_file_location('response_reconciliation_integration', PATH.with_name('delivery_response.py'))
|
||||
assert spec is not None and spec.loader is not None
|
||||
response = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(response)
|
||||
head = 'a' * 40
|
||||
state = {'writes': 0, 'body': '', 'head': head, 'number': 19, 'state': 'open',
|
||||
'base': 'main', 'repository': 'owner/repo', 'author': 6,
|
||||
'issue': {'body': 'approved', 'updated_at': 'pinned', 'comments': []},
|
||||
'files': ['one.py'], 'commits': [head], 'collections_complete': True}
|
||||
expected_issue = dict(state['issue'])
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def do_PATCH(self):
|
||||
state['writes'] += 1
|
||||
state['body'] = self.rfile.read(int(self.headers['Content-Length'])).decode()
|
||||
self.send_response(201)
|
||||
self.end_headers()
|
||||
self.wfile.write(b'{') # Committed, but malformed acknowledgement.
|
||||
def do_GET(self):
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(state).encode())
|
||||
def log_message(self, format, *args):
|
||||
pass
|
||||
server = HTTPServer(('127.0.0.1', 0), Handler)
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
url = 'http://127.0.0.1:' + str(server.server_port)
|
||||
def send(method='GET'):
|
||||
request = Request(url, data=b'intended' if method == 'PATCH' else None, method=method)
|
||||
with urlopen(request, timeout=2) as reply:
|
||||
return reply.status, reply.read(response.MAX_RESPONSE_BYTES + 1)
|
||||
def read():
|
||||
return response.deliver_once(send, expected_status=200)
|
||||
class Adapter(ReadOnlyAdapter):
|
||||
def read_remote_head(self):
|
||||
return read()['head']
|
||||
def read_pr(self, number):
|
||||
return read()
|
||||
def verify_reconciliation(self, head, number):
|
||||
actual = read()
|
||||
return (actual['body'] == 'intended' and actual['head'] == head
|
||||
and actual['number'] == number and actual['state'] == 'open'
|
||||
and actual['base'] == 'main' and actual['repository'] == 'owner/repo'
|
||||
and actual['author'] == 6 and actual['issue'] == expected_issue
|
||||
and actual['files'] == ['one.py'] and actual['commits'] == [head]
|
||||
and actual['collections_complete'] is True)
|
||||
try:
|
||||
with self.assertRaises(response.AcknowledgementUncertainError):
|
||||
response.deliver_once(lambda: send('PATCH'), expected_status=201)
|
||||
result = self.module.reconcile_implementation(Adapter(head), head=head, number=19, declaration={'mode': 'none'})
|
||||
self.assertEqual(result['remote_head'], head)
|
||||
for key, bad in [('body', 'competing change'), ('issue', {}), ('collections_complete', False)]:
|
||||
saved = state[key]
|
||||
state[key] = bad
|
||||
with self.subTest(key=key), self.assertRaises(self.module.AdoptionError):
|
||||
self.module.reconcile_implementation(Adapter(head), head=head, number=19, declaration={'mode': 'none'})
|
||||
state[key] = saved
|
||||
self.assertEqual(state['writes'], 1)
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
thread.join(timeout=2)
|
||||
self.assertFalse(thread.is_alive())
|
||||
|
||||
def test_closed_pr_cannot_complete(self):
|
||||
adapter = ReadOnlyAdapter('a' * 40)
|
||||
adapter.read_pr = lambda number: {'number': number, 'head': adapter.head, 'state': 'closed'}
|
||||
with self.assertRaises(self.module.AdoptionError):
|
||||
self.module.reconcile_implementation(adapter, head=adapter.head, number=19, declaration={'mode': 'none'})
|
||||
self.assertNotIn('complete_intent', adapter.calls)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user