import copy import hashlib import importlib.util import json import os import stat import subprocess import sys import tempfile import textwrap import time import unittest from pathlib import Path ROOT = Path(__file__).resolve().parents[3] SCRIPT = ROOT / 'harness/agents/review/skills/gitea-code-review/scripts/native_review.py' FIXTURES = ROOT / 'harness/tests/review/test_native_review.py' def load(path, name): spec = importlib.util.spec_from_file_location(name, path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module class PublicationJournalProcessTests(unittest.TestCase): def setUp(self): self.m = load(SCRIPT, 'native_review_process_' + str(id(self))) self.fixtures = load(FIXTURES, 'native_review_process_fixture_' + str(id(self))) self.tmp = tempfile.TemporaryDirectory() self.addCleanup(self.tmp.cleanup) self.root = Path(self.tmp.name) / 'native-review-journals' self.m._journal.root_for = lambda anchor, root=self.root: root def plan_data(self): p, before, after, exact = self.fixtures.publication_fixture() return p, before, after, exact def declaration(self): return {'mode': 'none'} def run_child(self, script, *args, timeout=8): env = {'PATH': os.environ['PATH'], 'PYTHONPATH': str(ROOT), 'PYTHONDONTWRITEBYTECODE': '1'} return subprocess.run([sys.executable, '-c', script, *map(str, args)], env=env, capture_output=True, text=True, timeout=timeout) def child_prelude(self): return textwrap.dedent(f''' import copy, importlib.util, json, os, sys, time from pathlib import Path root = Path(sys.argv[1]); payload = json.loads(Path(sys.argv[2]).read_text()) script = Path({str(SCRIPT)!r}); fixtures_path = Path({str(FIXTURES)!r}) def load(path, name): spec = importlib.util.spec_from_file_location(name, path) m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m); return m m = load(script, 'native_child_' + str(os.getpid())) f = load(fixtures_path, 'fixture_child_' + str(os.getpid())) m._journal.root_for = lambda anchor: root p, before, after, exact = f.publication_fixture() class Lease: def check(self, plan): return None class API: def __init__(self): self.created = Path(sys.argv[3]).exists() def identity(self): return copy.deepcopy(p['reviewer']) def snapshot(self, target): return copy.deepcopy(after if self.created else before) def read_review(self, target, rid): return copy.deepcopy(exact) def create_review(self, target, body): counter = Path(sys.argv[3]); n = int(counter.read_text() or '0') if counter.exists() else 0 counter.write_text(str(n + 1)) with counter.open('r+') as fd: fd.flush(); os.fsync(fd.fileno()) self.created = True return copy.deepcopy(exact) api = API() ''') def test_pending_intent_survives_process_crash_and_blocks_second_post(self): p, before, after, exact = self.plan_data() plan_file = Path(self.tmp.name) / 'plan.json'; plan_file.write_text(json.dumps(p, ensure_ascii=False)) counter = Path(self.tmp.name) / 'posts' crash = self.child_prelude() + textwrap.dedent(''' class CrashAPI(API): def create_review(self, target, body): Path(sys.argv[4]).write_text('create-entered') os._exit(17) m.publish(p, before, CrashAPI(), Lease(), m.canonical_journal_path(p), declaration={'mode': 'none'}) ''') entered = Path(self.tmp.name) / 'entered' crashed = self.run_child(crash, self.root, plan_file, counter, entered) self.assertEqual(crashed.returncode, 17) self.assertEqual(entered.read_text(), 'create-entered') resume = self.child_prelude() + textwrap.dedent(''' try: m.publish(p, before, API(), Lease(), m.canonical_journal_path(p), declaration={'mode': 'none'}) except Exception as exc: print(type(exc).__name__) ''') resumed = self.run_child(resume, self.root, plan_file, counter) self.assertEqual(resumed.returncode, 0, resumed.stderr) self.assertIn('UncertainPublication', resumed.stdout) self.assertFalse(counter.exists(), 'resume must not issue a second POST') def test_process_lock_contention_fails_fast_without_post(self): p, before, after, exact = self.plan_data() plan_file = Path(self.tmp.name) / 'plan.json'; plan_file.write_text(json.dumps(p)) counter = Path(self.tmp.name) / 'posts' ready = Path(self.tmp.name) / 'ready' holder = self.child_prelude() + textwrap.dedent(''' entry = m._journal.open_binding(m.canonical_journal_path(p), p, anchor_file=str(script), origin=m.ORIGIN, validate=m.validate_plan) fd = m._journal.acquire_lock(entry); Path(sys.argv[4]).write_text('ready'); time.sleep(3); os.close(fd) ''') proc = subprocess.Popen([sys.executable, '-c', holder, str(self.root), str(plan_file), str(counter), str(ready)], env={'PATH': os.environ['PATH'], 'PYTHONPATH': str(ROOT)}, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) try: deadline = time.monotonic() + 3 while not ready.exists() and time.monotonic() < deadline: time.sleep(.02) self.assertTrue(ready.exists()) contender = self.child_prelude() + textwrap.dedent(''' try: m.publish(p, before, API(), Lease(), m.canonical_journal_path(p), declaration={'mode': 'none'}) except Exception as exc: print(type(exc).__name__) ''') started = time.monotonic(); result = self.run_child(contender, self.root, plan_file, counter, timeout=2) self.assertLess(time.monotonic() - started, 2) self.assertEqual(result.returncode, 0, result.stderr) self.assertIn('ValidationError', result.stdout) self.assertFalse(counter.exists()) finally: proc.kill(); proc.communicate(timeout=3) def test_crash_after_transport_write_reconciles_without_second_write(self): p, _, _, _ = self.plan_data() plan_file = Path(self.tmp.name) / 'plan.json'; plan_file.write_text(json.dumps(p)) counter = Path(self.tmp.name) / 'posts' crash = self.child_prelude() + textwrap.dedent(""" class CrashAfterWrite(API): def create_review(self, target, body): super().create_review(target, body) os._exit(17) m.publish(p, before, CrashAfterWrite(), Lease(), declaration={'mode': 'none'}) """) result = self.run_child(crash, self.root, plan_file, counter) self.assertEqual(result.returncode, 17, result.stderr) self.assertEqual(counter.read_text(), '1') resume = self.child_prelude() + textwrap.dedent(""" result = m.publish(p, before, API(), Lease(), declaration={'mode': 'none'}) print(result['mode']) """) result = self.run_child(resume, self.root, plan_file, counter) self.assertEqual(result.returncode, 0, result.stderr) self.assertIn('no_op', result.stdout) self.assertEqual(counter.read_text(), '1') def test_two_publishing_processes_create_only_once(self): p, _, _, _ = self.plan_data() plan_file = Path(self.tmp.name) / 'plan.json'; plan_file.write_text(json.dumps(p)) counter = Path(self.tmp.name) / 'posts' entered = Path(self.tmp.name) / 'create-entered'; release = Path(self.tmp.name) / 'release' first = self.child_prelude() + textwrap.dedent(""" class GatedAPI(API): def create_review(self, target, body): result = super().create_review(target, body) Path(sys.argv[4]).write_text('entered') deadline = time.monotonic() + 8 while not Path(sys.argv[5]).exists(): if time.monotonic() >= deadline: raise TimeoutError('test watchdog') time.sleep(.02) return result result = m.publish(p, before, GatedAPI(), Lease(), declaration={'mode': 'none'}) print(result['mode']) """) proc = subprocess.Popen([sys.executable, '-B', '-c', first, str(self.root), str(plan_file), str(counter), str(entered), str(release)], env={'PATH': os.environ['PATH'], 'PYTHONPATH': str(ROOT)}, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) try: deadline = time.monotonic() + 4 while not entered.exists() and time.monotonic() < deadline: time.sleep(.02) self.assertTrue(entered.exists(), 'first publisher must reach transport') self.assertEqual(counter.read_text(), '1') contender = self.child_prelude() + textwrap.dedent(""" try: m.publish(p, before, API(), Lease(), declaration={'mode': 'none'}) except m.ValidationError: print('blocked') """) result = self.run_child(contender, self.root, plan_file, counter, timeout=3) self.assertEqual(result.returncode, 0, result.stderr) self.assertIn('blocked', result.stdout) self.assertEqual(counter.read_text(), '1') release.write_text('release') stdout, stderr = proc.communicate(timeout=3) self.assertEqual(proc.returncode, 0, stderr) self.assertIn('created', stdout) self.assertEqual(counter.read_text(), '1') finally: if proc.poll() is None: proc.kill() proc.communicate(timeout=3) def _legacy_files(self, p, before, *, verified=True): directory = Path(tempfile.mkdtemp(prefix='legacy-', dir=self.tmp.name)) directory.chmod(0o700) canonical = self.m.canonical intent = {'schema': 'gitea-agent.native-review-intent/v1', 'fingerprint': hashlib.sha256(self.m.canonical(p)).hexdigest(), 'before_sha256': hashlib.sha256(self.m.canonical(before)).hexdigest(), 'head': p['target']['head'], 'event': 'APPROVED', 'body_sha256': hashlib.sha256(p['body'].encode()).hexdigest()} with self.m.Journal(directory) as journal: journal.write('before.json', before) journal.write('intent.json', intent) if verified: journal.write('verified.json', {'verified': True}) return directory def _legacy_digest(self, directory): helper = getattr(self.m, 'legacy_journal_digest', None) self.assertTrue(callable(helper), 'public legacy journal digest helper is required') return helper(directory) def test_valid_legacy_registration_is_read_only_and_preserves_source_bytes(self): p, before, after, exact = self.plan_data(); source = self._legacy_files(p, before) original = {name: (source / name).read_bytes() for name in ['before.json', 'intent.json', 'verified.json']} modes = {name: stat.S_IMODE((source / name).stat().st_mode) for name in original} api = self.fixtures.FakeAPI(p, before, after, exact) result = self.m.register_legacy_journal(p, source, expected_legacy_sha256=self._legacy_digest(source), api=api, lease=self.fixtures.FakeLease(), declaration=self.declaration()) self.assertTrue(result['read_only']); self.assertEqual(api.posts, 0) for name, data in original.items(): self.assertEqual((source / name).read_bytes(), data) self.assertEqual(stat.S_IMODE((source / name).stat().st_mode), modes[name]) target = self.m.canonical_journal_path(p) self.assertEqual((target / 'before.json').read_bytes(), original['before.json']) self.assertFalse((target / 'import.pending.json').exists()) def test_legacy_source_mutation_digest_mismatch_and_extra_intent_fields_reject(self): p, before, after, exact = self.plan_data(); source = self._legacy_files(p, before) digest = self._legacy_digest(source) mutated = json.loads((source / 'before.json').read_text()); mutated['issue']['body'] = 'changed' (source / 'before.json').write_text(json.dumps(mutated, separators=(',', ':'))) with self.assertRaises(self.m.ValidationError): self.m.register_legacy_journal(p, source, expected_legacy_sha256=digest, api=self.fixtures.FakeAPI(p, before, after, exact), lease=self.fixtures.FakeLease(), declaration=self.declaration()) source = self._legacy_files(p, before) intent = json.loads((source / 'intent.json').read_text()); intent['unexpected'] = True (source / 'intent.json').write_text(json.dumps(intent, separators=(',', ':'))) with self.assertRaises(self.m.ValidationError): self.m.register_legacy_journal(p, source, expected_legacy_sha256=self._legacy_digest(source), api=self.fixtures.FakeAPI(p, before, after, exact), lease=self.fixtures.FakeLease(), declaration=self.declaration()) def test_legacy_symlink_hardlink_and_duplicate_json_are_rejected(self): p, before, after, exact = self.plan_data() source = self._legacy_files(p, before) (source / 'before.json').unlink(); (source / 'before.json').symlink_to(Path(self.tmp.name) / 'elsewhere') with self.assertRaises(self.m.ValidationError): self.m.register_legacy_journal(p, source, expected_legacy_sha256='0' * 64, api=self.fixtures.FakeAPI(p, before, after, exact), lease=self.fixtures.FakeLease(), declaration=self.declaration()) source = self._legacy_files(p, before) os.link(source / 'before.json', source / 'duplicate.json') with self.assertRaises(self.m.ValidationError): self.m.register_legacy_journal(p, source, expected_legacy_sha256=self._legacy_digest(source), api=self.fixtures.FakeAPI(p, before, after, exact), lease=self.fixtures.FakeLease(), declaration=self.declaration()) def test_registration_rejects_existing_canonical_binding_without_post(self): p, before, after, exact = self.plan_data(); source = self._legacy_files(p, before) target = self.m.canonical_journal_path(p) target.parent.mkdir(parents=True, exist_ok=True); target.mkdir(mode=0o700) (target / 'intent.json').write_text('{}'); (target / 'intent.json').chmod(0o600) api = self.fixtures.FakeAPI(p, before, after, exact) with self.assertRaises(self.m.ValidationError): self.m.register_legacy_journal(p, source, expected_legacy_sha256=self._legacy_digest(source), api=api, lease=self.fixtures.FakeLease(), declaration=self.declaration()) self.assertEqual(api.posts, 0) if __name__ == '__main__': unittest.main()