[verified] Canonicalize native review publication journals
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import stat
|
||||
import tempfile
|
||||
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'
|
||||
FIXTURE = 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 LegacyJournalContractTests(unittest.TestCase):
|
||||
def test_legacy_intent_extra_fields_are_not_registered(self):
|
||||
m = load(SCRIPT, 'legacy_contract_publisher')
|
||||
f = load(FIXTURE, 'legacy_contract_fixture')
|
||||
plan, before, after, exact = f.publication_fixture()
|
||||
with tempfile.TemporaryDirectory(dir='/opt/data/cache/scratch') as td:
|
||||
m._journal.root_for = lambda anchor: Path(td) / 'native-review-journals'
|
||||
source = Path(td) / 'legacy'
|
||||
payload = m.build_payload(plan)
|
||||
intent = {'schema': 'gitea-agent.native-review-intent/v1',
|
||||
'fingerprint': hashlib.sha256(m.canonical(plan)).hexdigest(),
|
||||
'before_sha256': hashlib.sha256(m.canonical(before)).hexdigest(),
|
||||
'head': plan['target']['head'], 'event': payload['event'],
|
||||
'body_sha256': hashlib.sha256(payload['body'].encode()).hexdigest(),
|
||||
'unexpected': 'cannot silently import this'}
|
||||
with m.Journal(source) as journal:
|
||||
journal.write('before.json', before)
|
||||
journal.write('intent.json', intent)
|
||||
digest = m.legacy_journal_digest(source)
|
||||
api = f.FakeAPI(plan, before, after, exact)
|
||||
with self.assertRaises(m.ValidationError):
|
||||
m.register_legacy_journal(plan, source, expected_legacy_sha256=digest,
|
||||
api=api, lease=f.FakeLease(), declaration={'mode': 'none'})
|
||||
self.assertEqual(api.posts, 0)
|
||||
self.assertFalse((m.canonical_journal_path(plan) / 'intent.json').exists())
|
||||
|
||||
def test_legacy_inventory_rejects_unknown_artifact_before_import(self):
|
||||
m = load(SCRIPT, 'legacy_inventory_contract')
|
||||
f = load(FIXTURE, 'legacy_inventory_fixture')
|
||||
plan, before, after, exact = f.publication_fixture()
|
||||
with tempfile.TemporaryDirectory(dir='/opt/data/cache/scratch') as td:
|
||||
m._journal.root_for = lambda anchor: Path(td) / 'native-review-journals'
|
||||
source = Path(td) / 'legacy'; source.mkdir(mode=0o700)
|
||||
with m.Journal(source) as journal:
|
||||
journal.write('before.json', before)
|
||||
journal.write('intent.json', m._expected_intent(plan, before))
|
||||
extra = source / 'unknown.json'; extra.write_bytes(b'{"unknown":true}')
|
||||
extra.chmod(0o600)
|
||||
with self.assertRaises(m.ValidationError):
|
||||
m.legacy_journal_digest(source)
|
||||
|
||||
def test_import_preserves_raw_bytes_and_archives_verified_separately(self):
|
||||
m = load(SCRIPT, 'legacy_raw_import_contract')
|
||||
f = load(FIXTURE, 'legacy_raw_import_fixture')
|
||||
plan, before, after, exact = f.publication_fixture()
|
||||
with tempfile.TemporaryDirectory(dir='/opt/data/cache/scratch') as td:
|
||||
m._journal.root_for = lambda anchor: Path(td) / 'native-review-journals'
|
||||
source = Path(td) / 'legacy'; source.mkdir(mode=0o700)
|
||||
intent = m._expected_intent(plan, before)
|
||||
before_raw = (json.dumps(before, sort_keys=True, indent=1) + '\n').encode()
|
||||
intent_raw = (json.dumps(intent, sort_keys=True, indent=1) + '\n').encode()
|
||||
verified_raw = b'{\n "verified": true,\n "legacy": "exact"\n}\n'
|
||||
for name, raw in [('before.json', before_raw), ('intent.json', intent_raw),
|
||||
('verified.json', verified_raw)]:
|
||||
path = source / name; path.write_bytes(raw); path.chmod(0o600)
|
||||
with m.Journal(source):
|
||||
pass
|
||||
original = {name: (source / name).read_bytes() for name in
|
||||
['before.json', 'intent.json', 'verified.json']}
|
||||
digest = m.legacy_journal_digest(source)
|
||||
result = m.register_legacy_journal(
|
||||
plan, source, expected_legacy_sha256=digest,
|
||||
api=f.FakeAPI(plan, before, after, exact), lease=f.FakeLease(),
|
||||
declaration={'mode': 'none'})
|
||||
self.assertTrue(result['read_only'])
|
||||
target = m.canonical_journal_path(plan)
|
||||
self.assertEqual((target / 'before.json').read_bytes(), original['before.json'])
|
||||
self.assertEqual((target / 'intent.json').read_bytes(), original['intent.json'])
|
||||
self.assertEqual((target / 'legacy-verified.json').read_bytes(), original['verified.json'])
|
||||
self.assertFalse((target / 'verified.json').exists())
|
||||
self.assertEqual(stat.S_IMODE((target / 'legacy-verified.json').stat().st_mode), 0o600)
|
||||
api = f.FakeAPI(plan, before, after, exact); api.created = True
|
||||
for _ in range(2):
|
||||
result = m.publish(plan, before, api, f.FakeLease(), declaration={'mode': 'none'})
|
||||
self.assertFalse(result['wrote'])
|
||||
self.assertEqual(api.posts, 0)
|
||||
self.assertTrue((target / 'verified.json').exists())
|
||||
self.assertEqual((target / 'legacy-verified.json').read_bytes(), verified_raw)
|
||||
(target / 'legacy-verified.json').write_bytes(b'{"verified": false}')
|
||||
with self.assertRaisesRegex(m.ValidationError, 'integrity mismatch'):
|
||||
m.publish(plan, before, api, f.FakeLease(), declaration={'mode': 'none'})
|
||||
self.assertEqual(api.posts, 0)
|
||||
|
||||
|
||||
|
||||
def test_receipt_cannot_omit_imported_verified_archive(self):
|
||||
m = load(SCRIPT, 'legacy_receipt_contract'); f = load(FIXTURE, 'legacy_receipt_fixture')
|
||||
plan, before, after, exact = f.publication_fixture()
|
||||
with tempfile.TemporaryDirectory(dir='/opt/data/cache/scratch') as td:
|
||||
m._journal.root_for = lambda anchor: Path(td) / 'native-review-journals'
|
||||
source = Path(td) / 'legacy'
|
||||
with m.Journal(source) as journal:
|
||||
journal.write('before.json', before)
|
||||
journal.write('intent.json', m._expected_intent(plan, before))
|
||||
journal.write('verified.json', {'verified': True})
|
||||
m.register_legacy_journal(plan, source, expected_legacy_sha256=m.legacy_journal_digest(source),
|
||||
api=f.FakeAPI(plan, before, after, exact), lease=f.FakeLease(), declaration={'mode': 'none'})
|
||||
target = m.canonical_journal_path(plan)
|
||||
receipt = json.loads((target / 'registration.json').read_text())
|
||||
receipt['imported_files'].pop()
|
||||
with m.Journal(target) as journal:
|
||||
journal.write('registration.json', receipt)
|
||||
with self.assertRaises(m.ValidationError): m._check_registration(journal, plan)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,29 @@
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
REFERENCE = ROOT / 'harness/agents/review/skills/gitea-code-review/references/native-review.md'
|
||||
SKILL = ROOT / 'harness/agents/review/skills/gitea-code-review/SKILL.md'
|
||||
CACHE = ROOT / 'harness/operations/repository-cache.md'
|
||||
|
||||
|
||||
class NativeJournalDocumentationTests(unittest.TestCase):
|
||||
def test_canonical_registry_and_legacy_policy_are_explicit(self):
|
||||
for path in (REFERENCE, SKILL, CACHE):
|
||||
text = path.read_text()
|
||||
for token in ('harness/.runtime/native-review-journals',
|
||||
'canonical_journal_path', 'register_legacy_journal',
|
||||
'binding', 'read-only', 'cross-host'):
|
||||
with self.subTest(path=str(path), token=token):
|
||||
self.assertIn(token, text)
|
||||
|
||||
def test_no_caller_selected_journal_in_live_example(self):
|
||||
text = REFERENCE.read_text()
|
||||
self.assertNotIn('result = native_review.publish(plan, baseline, api, lease, journal_directory)', text)
|
||||
self.assertIn('result = native_review.publish(plan, baseline, api, lease)', text)
|
||||
self.assertIn('기존 journal을 자동 이동·삭제', text)
|
||||
self.assertIn('pending', text)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -121,7 +121,18 @@ class FakeLease:
|
||||
class NativeReviewTests(unittest.TestCase):
|
||||
def module(self):
|
||||
self.assertTrue(SCRIPT.is_file(), 'shared native review adapter is missing')
|
||||
return load(SCRIPT, 'native_review_test')
|
||||
m = load(SCRIPT, 'native_review_test_' + str(id(self)))
|
||||
# Every test gets a private registry root; production has no test-only
|
||||
# root override, so isolate through the journal module seam.
|
||||
private_root = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(private_root.cleanup)
|
||||
root = Path(private_root.name) / 'native-review-journals'
|
||||
m._journal.root_for = lambda anchor, root=root: root
|
||||
return m
|
||||
|
||||
@staticmethod
|
||||
def canonical(m, plan):
|
||||
return m.canonical_journal_path(plan)
|
||||
|
||||
def test_payload_is_exact_bound_native_create(self):
|
||||
m = self.module()
|
||||
@@ -159,22 +170,23 @@ class NativeReviewTests(unittest.TestCase):
|
||||
p, before, after, exact = publication_fixture()
|
||||
api = FakeAPI(p, before, after, exact); lease = FakeLease()
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
result = m.publish(p, before, api, lease, td, declaration={'mode': 'none'})
|
||||
result = m.publish(p, before, api, lease, self.canonical(m, p), declaration={'mode': 'none'})
|
||||
self.assertEqual(result['mode'], 'created')
|
||||
self.assertEqual(api.posts, 1)
|
||||
self.assertEqual(m.publish(p, before, api, lease, td, declaration={'mode': 'none'})['mode'], 'no_op')
|
||||
self.assertEqual(m.publish(p, before, api, lease, self.canonical(m, p), declaration={'mode': 'none'})['mode'], 'no_op')
|
||||
self.assertEqual(api.posts, 1)
|
||||
self.assertNotIn('body', json.loads((Path(td) / 'intent.json').read_text()))
|
||||
self.assertNotIn('body', json.loads((self.canonical(m, p) / 'intent.json').read_text()))
|
||||
for applies in [True, False]:
|
||||
m = self.module(); p, before, after, exact = publication_fixture()
|
||||
api = FakeAPI(p, before, after, exact); api.timeout = True; api.apply = applies
|
||||
with self.subTest(applies=applies), tempfile.TemporaryDirectory() as td:
|
||||
if applies:
|
||||
self.assertEqual(m.publish(p, before, api, lease, td, declaration={'mode': 'none'})['mode'], 'reconciled')
|
||||
self.assertEqual(m.publish(p, before, api, lease, td, declaration={'mode': 'none'})['mode'], 'no_op')
|
||||
self.assertEqual(m.publish(p, before, api, lease, self.canonical(m, p), declaration={'mode': 'none'})['mode'], 'reconciled')
|
||||
self.assertEqual(m.publish(p, before, api, lease, self.canonical(m, p), declaration={'mode': 'none'})['mode'], 'no_op')
|
||||
else:
|
||||
for _ in range(2):
|
||||
with self.assertRaises(m.UncertainPublication) as error:
|
||||
m.publish(p, before, api, lease, td, declaration={'mode': 'none'})
|
||||
m.publish(p, before, api, lease, self.canonical(m, p), declaration={'mode': 'none'})
|
||||
self.assertNotIn('secret transport detail', str(error.exception))
|
||||
self.assertEqual(api.posts, 1)
|
||||
def test_api_adapter_checks_identity_first_and_collects_all_pages(self):
|
||||
@@ -333,9 +345,9 @@ class NativeReviewTests(unittest.TestCase):
|
||||
p, before, after, exact = publication_fixture(); api = FakeAPI(p, before, after, exact); lease = FakeLease()
|
||||
if kind == 'expired': lease.valid = False
|
||||
else: api.identity = lambda: p['spec_reviewer']
|
||||
with self.assertRaises(m.ValidationError): m.publish(p, before, api, lease, td, declaration={'mode': 'none'})
|
||||
with self.assertRaises(m.ValidationError): m.publish(p, before, api, lease, self.canonical(m, p), declaration={'mode': 'none'})
|
||||
self.assertEqual(api.posts, 0)
|
||||
self.assertFalse((Path(td) / 'intent.json').exists())
|
||||
self.assertFalse((self.canonical(m, p) / 'intent.json').exists())
|
||||
|
||||
def test_fresh_head_contract_and_source_linkage_drift_prevent_post(self):
|
||||
m = self.module()
|
||||
@@ -348,7 +360,7 @@ class NativeReviewTests(unittest.TestCase):
|
||||
with self.subTest(case=i), tempfile.TemporaryDirectory() as td:
|
||||
p, before, after, exact = publication_fixture(); api = FakeAPI(p, copy.deepcopy(before), after, exact)
|
||||
change(api.before)
|
||||
with self.assertRaises(m.ValidationError): m.publish(p, before, api, FakeLease(), td, declaration={'mode': 'none'})
|
||||
with self.assertRaises(m.ValidationError): m.publish(p, before, api, FakeLease(), self.canonical(m, p), declaration={'mode': 'none'})
|
||||
self.assertEqual(api.posts, 0)
|
||||
|
||||
def test_pagination_total_duplicate_and_truncation_are_hard_errors(self):
|
||||
@@ -424,10 +436,10 @@ class NativeReviewTests(unittest.TestCase):
|
||||
m = self.module(); p, before, after, exact = publication_fixture()
|
||||
api = FakeAPI(p, before, after, exact); api.created = True
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
result = m.publish(p, after, api, FakeLease(), td, declaration={'mode': 'none'})
|
||||
result = m.publish(p, after, api, FakeLease(), self.canonical(m, p), declaration={'mode': 'none'})
|
||||
self.assertEqual(result['mode'], 'no_op'); self.assertFalse(result['wrote'])
|
||||
self.assertEqual(api.posts, 0)
|
||||
self.assertFalse((Path(td) / 'intent.json').exists())
|
||||
self.assertFalse((self.canonical(m, p) / 'intent.json').exists())
|
||||
|
||||
def test_duplicate_or_conflicting_canonical_review_is_not_reposted(self):
|
||||
m = self.module()
|
||||
@@ -441,7 +453,7 @@ class NativeReviewTests(unittest.TestCase):
|
||||
after['pagination']['reviews'][0].update(count=4, declared_total=4)
|
||||
after['pagination']['review_comments_11'] = [{'page': 1, 'count': 0, 'declared_total': 0, 'link': None}]
|
||||
api = FakeAPI(p, before, after, exact); api.created = True
|
||||
with self.assertRaises(m.ValidationError): m.publish(p, before, api, FakeLease(), td, declaration={'mode': 'none'})
|
||||
with self.assertRaises(m.ValidationError): m.publish(p, before, api, FakeLease(), self.canonical(m, p), declaration={'mode': 'none'})
|
||||
self.assertEqual(api.posts, 0)
|
||||
|
||||
def test_readback_failure_and_changed_journal_plan_never_retry_post(self):
|
||||
@@ -449,7 +461,7 @@ class NativeReviewTests(unittest.TestCase):
|
||||
api.read_review = lambda target, rid: (_ for _ in ()).throw(TimeoutError('DO_NOT_ECHO_TRANSPORT_SECRET'))
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
for _ in range(2):
|
||||
with self.assertRaises(Exception) as raised: m.publish(p, before, api, FakeLease(), td, declaration={'mode': 'none'})
|
||||
with self.assertRaises(Exception) as raised: m.publish(p, before, api, FakeLease(), self.canonical(m, p), declaration={'mode': 'none'})
|
||||
self.assertNotIn('DO_NOT_ECHO_TRANSPORT_SECRET', str(raised.exception))
|
||||
self.assertEqual(api.posts, 1)
|
||||
different = copy.deepcopy(p); different['body'] += 'different'
|
||||
@@ -462,15 +474,15 @@ class NativeReviewTests(unittest.TestCase):
|
||||
root = Path(td); link = root / 'link'; link.symlink_to(root, target_is_directory=True)
|
||||
with self.assertRaises(m.ValidationError): m.Journal(link)
|
||||
api = FakeAPI(p, before, after, exact); api.timeout = True; api.apply = False
|
||||
journal = root / 'journal'
|
||||
journal = self.canonical(m, p)
|
||||
with self.assertRaises(m.UncertainPublication): m.publish(p, before, api, FakeLease(), journal, declaration={'mode': 'none'})
|
||||
saved = json.loads((journal / 'before.json').read_text()); saved['issue']['body'] = 'changed'
|
||||
(journal / 'before.json').write_text(json.dumps(saved))
|
||||
with self.assertRaises(m.ValidationError): m.publish(p, before, api, FakeLease(), journal, declaration={'mode': 'none'})
|
||||
self.assertEqual(api.posts, 1)
|
||||
with m.Journal(root / 'hardlinks') as j:
|
||||
j.write('a.json', {'safe': True}); os.link(j.path / 'a.json', j.path / 'b.json')
|
||||
with self.assertRaises(m.ValidationError): j.read('a.json')
|
||||
j.write('before.json', {'safe': True}); os.link(j.path / 'before.json', j.path / 'b.json')
|
||||
with self.assertRaises(m.ValidationError): j.read('before.json')
|
||||
|
||||
def test_transport_origin_credential_source_and_exception_text_are_guarded(self):
|
||||
m = self.module()
|
||||
@@ -485,7 +497,7 @@ class NativeReviewTests(unittest.TestCase):
|
||||
m = self.module(); p, before, after, exact = publication_fixture(); api = FakeAPI(p, before, after, exact)
|
||||
lease = FakeLease(); lease.check = lambda plan: False
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
with self.assertRaises(m.ValidationError): m.publish(p, before, api, lease, td, declaration={'mode': 'none'})
|
||||
with self.assertRaises(m.ValidationError): m.publish(p, before, api, lease, self.canonical(m, p), declaration={'mode': 'none'})
|
||||
self.assertEqual(api.posts, 0)
|
||||
def test_live_rest_bridge_over_loopback_publishes_once_then_no_ops(self):
|
||||
import threading
|
||||
@@ -533,10 +545,10 @@ class NativeReviewTests(unittest.TestCase):
|
||||
api = m.ApiAdapter(request, origin=m.ORIGIN + '/api/v1', credential_source='$HERMES_HOME/.env:GITEA_TOKEN')
|
||||
baseline = api.snapshot(p['target'])
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
result = m.publish(p, baseline, api, FakeLease(), td, declaration={'mode': 'none'})
|
||||
result = m.publish(p, baseline, api, FakeLease(), self.canonical(m, p), declaration={'mode': 'none'})
|
||||
self.assertEqual(result['mode'], 'created'); self.assertEqual(result['projected_review_ids'], [8])
|
||||
self.assertTrue(result['commit_file_order_normalized'])
|
||||
second = m.publish(p, baseline, api, FakeLease(), td, declaration={'mode': 'none'})
|
||||
second = m.publish(p, baseline, api, FakeLease(), self.canonical(m, p), declaration={'mode': 'none'})
|
||||
self.assertEqual(second['mode'], 'no_op'); self.assertFalse(second['wrote'])
|
||||
self.assertEqual(state['posts'], 1); self.assertEqual(server.received, m.build_payload(p))
|
||||
finally:
|
||||
@@ -586,15 +598,15 @@ class NativeReviewTests(unittest.TestCase):
|
||||
source.assert_not_called()
|
||||
|
||||
def test_journal_intent_fields_are_validated_on_resume(self):
|
||||
m = self.module()
|
||||
for field in ['schema', 'head', 'event', 'body_sha256']:
|
||||
m = self.module()
|
||||
with self.subTest(field=field), tempfile.TemporaryDirectory() as td:
|
||||
p, before, after, exact = publication_fixture(); api = FakeAPI(p, before, after, exact)
|
||||
api.timeout = True; api.apply = False
|
||||
with self.assertRaises(m.UncertainPublication): m.publish(p, before, api, FakeLease(), td, declaration={'mode': 'none'})
|
||||
path = Path(td) / 'intent.json'; intent = json.loads(path.read_text()); intent[field] = 'corrupt'
|
||||
with self.assertRaises(m.ValidationError): m.publish(p, before, api, FakeLease(), self.canonical(m, p), declaration={'mode': 'none'})
|
||||
path = self.canonical(m, p) / 'intent.json'; intent = json.loads(path.read_text()); intent[field] = 'corrupt'
|
||||
path.write_text(json.dumps(intent)); api.created = True
|
||||
with self.assertRaises(m.ValidationError): m.publish(p, before, api, FakeLease(), td, declaration={'mode': 'none'})
|
||||
with self.assertRaises(m.ValidationError): m.publish(p, before, api, FakeLease(), self.canonical(m, p), declaration={'mode': 'none'})
|
||||
self.assertEqual(api.posts, 1)
|
||||
|
||||
def test_issue_repository_mismatch_is_not_accepted(self):
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import copy
|
||||
import importlib.util
|
||||
import os
|
||||
import tempfile
|
||||
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 CanonicalJournalTests(unittest.TestCase):
|
||||
def test_binding_path_is_stable_and_head_scoped(self):
|
||||
m = load(SCRIPT, 'native_review_canonical_test')
|
||||
fixtures = load(FIXTURES, 'native_review_fixture_test')
|
||||
plan = fixtures.plan_fixture()
|
||||
first = m.canonical_journal_path(plan)
|
||||
again = m.canonical_journal_path(copy.deepcopy(plan))
|
||||
self.assertEqual(first, again)
|
||||
changed = copy.deepcopy(plan)
|
||||
changed['target']['head'] = 'b' * 40
|
||||
changed['body'] = changed['body'].replace('head=' + plan['target']['head'], 'head=' + changed['target']['head'])
|
||||
self.assertNotEqual(first, m.canonical_journal_path(changed))
|
||||
changed = copy.deepcopy(plan)
|
||||
changed['reviewer']['id'] = 43
|
||||
self.assertNotEqual(first, m.canonical_journal_path(changed))
|
||||
self.assertEqual(first.parts[-2], 'v1')
|
||||
self.assertEqual(first.parts[-3], 'native-review-journals')
|
||||
|
||||
def test_missing_entry_with_presence_anchor_fails_closed(self):
|
||||
m = load(SCRIPT, 'native_review_anchor_test')
|
||||
fixtures = load(FIXTURES, 'native_review_anchor_fixture_test')
|
||||
plan = fixtures.plan_fixture()
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
root = Path(td) / 'native-review-journals'
|
||||
old = m._journal.root_for
|
||||
m._journal.root_for = lambda anchor: root
|
||||
try:
|
||||
path = m.canonical_journal_path(plan)
|
||||
entry = m._journal.open_binding(path, plan, anchor_file=str(SCRIPT),
|
||||
origin=m.ORIGIN, validate=m.validate_plan)
|
||||
self.assertTrue((entry / 'binding.json').exists())
|
||||
import shutil
|
||||
shutil.rmtree(entry)
|
||||
with self.assertRaises(ValueError):
|
||||
m._journal.open_binding(path, plan, anchor_file=str(SCRIPT),
|
||||
origin=m.ORIGIN, validate=m.validate_plan)
|
||||
finally:
|
||||
m._journal.root_for = old
|
||||
|
||||
def test_deleted_root_with_persistent_anchor_fails_closed(self):
|
||||
m = load(SCRIPT, 'native_review_root_loss_test')
|
||||
fixtures = load(FIXTURES, 'native_review_root_loss_fixture_test')
|
||||
plan = fixtures.plan_fixture()
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
root = Path(td) / 'native-review-journals'
|
||||
old = m._journal.root_for
|
||||
m._journal.root_for = lambda anchor: root
|
||||
try:
|
||||
path = m.canonical_journal_path(plan)
|
||||
m._journal.open_binding(path, plan, anchor_file=str(SCRIPT),
|
||||
origin=m.ORIGIN, validate=m.validate_plan)
|
||||
import shutil
|
||||
shutil.rmtree(root)
|
||||
with self.assertRaises(ValueError):
|
||||
m._journal.open_binding(path, plan, anchor_file=str(SCRIPT),
|
||||
origin=m.ORIGIN, validate=m.validate_plan)
|
||||
finally:
|
||||
m._journal.root_for = old
|
||||
|
||||
def test_alternate_path_is_rejected_before_publication(self):
|
||||
m = load(SCRIPT, 'native_review_alternate_test')
|
||||
fixtures = load(FIXTURES, 'native_review_alternate_fixture_test')
|
||||
plan, before, after, exact = fixtures.publication_fixture()
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
with self.assertRaises(m.ValidationError):
|
||||
m.publish(plan, before, fixtures.FakeAPI(plan, before, after, exact),
|
||||
fixtures.FakeLease(), td, declaration={'mode': 'none'})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,277 @@
|
||||
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()
|
||||
@@ -0,0 +1,125 @@
|
||||
import importlib.util
|
||||
import json
|
||||
import multiprocessing
|
||||
import os
|
||||
import stat
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT = Path(__file__).resolve().parents[2] / 'agents/review/skills/gitea-code-review/scripts/publication_journal.py'
|
||||
|
||||
def load():
|
||||
spec = importlib.util.spec_from_file_location('publication_journal_safety', SCRIPT)
|
||||
m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m); return m
|
||||
|
||||
def plan(n='a'):
|
||||
return {'target': {'owner':'o','repository':'r','issue':1,'pull':2,'head':n*40},
|
||||
'contract_sha256':'b'*64, 'reviewer': {'id':3}}
|
||||
|
||||
def validate(p): return p
|
||||
|
||||
class SafetyTests(unittest.TestCase):
|
||||
def test_binding_record_contains_plan_fingerprint_and_strict_schema(self):
|
||||
m=load()
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
root=Path(td)/'root'; m.root_for=lambda _: root
|
||||
p=plan(); e=m.open_binding(m.canonical_path(p,anchor_file=__file__,origin='x',validate=validate),p,anchor_file=__file__,origin='x',validate=validate)
|
||||
record=json.loads((e/'binding.json').read_text())
|
||||
self.assertEqual(set(record), {'schema','binding','fingerprint'})
|
||||
self.assertEqual(record['fingerprint'], m.plan_fingerprint(p))
|
||||
self.assertEqual(m.assert_bound(e,p,anchor_file=__file__,origin='x',validate=validate), e)
|
||||
|
||||
def test_different_plan_same_binding_conflicts_without_overwrite(self):
|
||||
m=load()
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
root=Path(td)/'root'; m.root_for=lambda _: root
|
||||
p=plan(); path=m.canonical_path(p,anchor_file=__file__,origin='x',validate=validate); m.open_binding(path,p,anchor_file=__file__,origin='x',validate=validate)
|
||||
q=dict(p); q['extra']='different'
|
||||
with self.assertRaises(ValueError): m.assert_bound(path,q,anchor_file=__file__,origin='x',validate=validate)
|
||||
|
||||
def test_strict_json_rejects_duplicate_and_nonfinite(self):
|
||||
m=load()
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
p=Path(td)/'x'; p.write_bytes(b'{"a":1,"a":2}') ; os.chmod(p,0o600)
|
||||
with self.assertRaises(ValueError): m._strict_load(m._secure_file(p))
|
||||
p.write_bytes(b'{"a":NaN}')
|
||||
with self.assertRaises(ValueError): m._strict_load(m._secure_file(p))
|
||||
p.write_bytes(b'{"a":1e999}')
|
||||
with self.assertRaises(ValueError): m._strict_load(m._secure_file(p))
|
||||
|
||||
def test_require_new_seeds_exact_pending_registration_sentinel(self):
|
||||
m=load()
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
root=Path(td)/'root'; m.root_for=lambda _: root
|
||||
p=plan(); path=m.canonical_path(p,anchor_file=__file__,origin='x',validate=validate)
|
||||
entry=m.open_binding(path,p,anchor_file=__file__,origin='x',validate=validate,require_new=True)
|
||||
self.assertEqual(json.loads((entry/'import.pending.json').read_text()), {'schema':'gitea-agent.native-review-import/v1','state':'pending'})
|
||||
with self.assertRaises(ValueError):
|
||||
m.open_binding(path,p,anchor_file=__file__,origin='x',validate=validate)
|
||||
|
||||
def test_fifo_does_not_block_and_symlink_hardlink_rejected(self):
|
||||
m=load()
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
d=Path(td); fifo=d/'fifo'; os.mkfifo(fifo,0o600)
|
||||
with self.assertRaises(ValueError): m._secure_file(fifo,cap=10)
|
||||
real=d/'real'; real.write_bytes(b'{}'); os.chmod(real,0o600)
|
||||
os.symlink(real,d/'link')
|
||||
with self.assertRaises(Exception): m._secure_file(d/'link')
|
||||
os.link(real,d/'hard')
|
||||
with self.assertRaises(ValueError): m._secure_file(real)
|
||||
|
||||
def test_deleted_binding_record_is_not_reconstructed(self):
|
||||
m = load()
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
m.root_for = lambda _: Path(td) / 'root'
|
||||
p = plan(); path = m.canonical_path(p, anchor_file=__file__, origin='x', validate=validate)
|
||||
m.open_binding(path, p, anchor_file=__file__, origin='x', validate=validate)
|
||||
(path / 'binding.json').unlink()
|
||||
with self.assertRaises(ValueError):
|
||||
m.open_binding(path, p, anchor_file=__file__, origin='x', validate=validate)
|
||||
self.assertFalse((path / 'binding.json').exists())
|
||||
|
||||
def test_first_use_bootstraps_private_runtime_parent(self):
|
||||
m = load()
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
runtime = Path(td) / '.runtime'
|
||||
m.root_for = lambda _: runtime / 'root'
|
||||
p = plan(); path = m.canonical_path(p, anchor_file=__file__, origin='x', validate=validate)
|
||||
self.assertEqual(m.open_binding(path, p, anchor_file=__file__, origin='x', validate=validate), path)
|
||||
self.assertEqual(stat.S_IMODE(runtime.stat().st_mode), 0o700)
|
||||
|
||||
def test_prewrite_presence_validation_rejects_schema_corruption(self):
|
||||
m = load()
|
||||
for kind in ('root', 'entry'):
|
||||
with self.subTest(kind=kind), tempfile.TemporaryDirectory() as td:
|
||||
m.root_for = lambda _: Path(td) / 'root'
|
||||
p = plan(); path = m.canonical_path(p, anchor_file=__file__, origin='x', validate=validate)
|
||||
m.open_binding(path, p, anchor_file=__file__, origin='x', validate=validate)
|
||||
marker = Path(td) / ('root.presence' if kind == 'root' else path.name + '.presence')
|
||||
record = json.loads(marker.read_text()); record['schema'] = 'corrupt'
|
||||
marker.write_text(json.dumps(record))
|
||||
with self.assertRaises(ValueError):
|
||||
m.assert_bound(path, p, anchor_file=__file__, origin='x', validate=validate)
|
||||
|
||||
def test_unsafe_mutex_failure_does_not_leak_descriptor(self):
|
||||
m = load()
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
path = Path(td) / 'publication.lock'; path.write_text(''); path.chmod(0o644)
|
||||
def live_fds():
|
||||
found = set()
|
||||
for name in os.listdir('/proc/self/fd'):
|
||||
try: os.readlink('/proc/self/fd/' + name)
|
||||
except FileNotFoundError: continue
|
||||
found.add(name)
|
||||
return found
|
||||
before = live_fds()
|
||||
with self.assertRaises(ValueError): m.acquire_lock(Path(td))
|
||||
after = live_fds()
|
||||
leaked = after - before
|
||||
for value in leaked:
|
||||
try: os.close(int(value))
|
||||
except OSError: pass
|
||||
self.assertEqual(after, before)
|
||||
|
||||
if __name__ == '__main__': unittest.main()
|
||||
Reference in New Issue
Block a user