test(harness): reorganize regression suites and preserve contract coverage

This commit is contained in:
2026-10-16 07:24:49 +00:00
parent b6f87db623
commit 33709ab873
108 changed files with 5869 additions and 5328 deletions
@@ -0,0 +1 @@
"""repository regression tests."""
@@ -0,0 +1,151 @@
"""Synthetic local Git pre-fetch gate checks; no provider/network operations."""
from harness.tests._support.paths import ROOT as SOURCE_ROOT
import json
import os
import subprocess
import tempfile
import unittest
from pathlib import Path
ROOT = SOURCE_ROOT
SCRIPT = ROOT / 'harness/lib/repository_prefetch.py'
class RepositoryPrefetchTests(unittest.TestCase):
def git(self, *args):
result = subprocess.run(['/usr/bin/git', '-c', 'core.hooksPath=/dev/null', *map(str, args)],
env=self.env, capture_output=True, text=True, timeout=10)
self.assertEqual(result.returncode, 0, result.stderr)
return result.stdout
def setUp(self):
self.temp = tempfile.TemporaryDirectory(dir=os.environ.get('TMPDIR', '/opt/data/cache/scratch'))
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
self.env = {'PATH': '/usr/bin:/bin', 'HOME': str(self.root), 'GIT_CONFIG_NOSYSTEM': '1',
'GIT_CONFIG_GLOBAL': '/dev/null', 'GIT_TERMINAL_PROMPT': '0'}
self.seed = self.root / 'seed'
self.bare = self.root / 'mirror.git'
self.git('init', '-q', self.seed)
self.git('-C', self.seed, 'config', 'user.name', 'Synthetic')
self.git('-C', self.seed, 'config', 'user.email', 'fixture@example.invalid')
(self.seed / 'a').write_text('synthetic')
self.git('-C', self.seed, 'add', 'a')
self.git('-C', self.seed, 'commit', '-qm', 'synthetic')
self.git('clone', '--mirror', self.seed, self.bare)
def gate(self):
self.assertTrue(SCRIPT.is_file(), 'canonical read-only prefetch helper is missing')
result = subprocess.run(['python3', '-B', str(SCRIPT), '--git-dir', str(self.bare)],
env=self.env, capture_output=True, text=True, timeout=15)
return result.returncode, json.loads(result.stdout)
def test_bare_only_passes_real_git_read_only(self):
before = self.git('--git-dir', self.bare, 'show-ref')
code, value = self.gate()
self.assertEqual(code, 0)
self.assertEqual(value, {'status': 'pass', 'attached_worktrees': [], 'reason': 'no_attached_worktrees'})
self.assertEqual(self.git('--git-dir', self.bare, 'show-ref'), before)
def test_attached_blocks_exact_branch_and_path_without_mutation(self):
checkout = self.root / 'attached space\npath'
self.git('--git-dir', self.bare, 'worktree', 'add', '-b', 'issue-fixture', checkout, 'HEAD')
self.git('--git-dir', self.bare, 'worktree', 'lock', checkout)
state = checkout.parent / 'state.json'
state.write_text('sentinel')
before = self.fingerprint(self.bare, checkout, state)
code, value = self.gate()
self.assertEqual(code, 1)
self.assertEqual(value, {'status': 'block', 'attached_worktrees': [
{'branch': 'refs/heads/issue-fixture', 'path': str(checkout)}], 'reason': 'attached_worktree'})
self.assertEqual(self.fingerprint(self.bare, checkout, state), before)
def test_detached_locked_worktree_passes_without_mutation(self):
checkout = self.root / 'detached'
self.git('--git-dir', self.bare, 'worktree', 'add', '--detach', '--lock', checkout, 'HEAD')
before = self.fingerprint(self.bare, checkout)
code, value = self.gate()
self.assertEqual(code, 0)
self.assertEqual(value['attached_worktrees'], [])
self.assertEqual(self.fingerprint(self.bare, checkout), before)
def test_mixed_worktrees_report_all_attached_without_branch_name_policy(self):
detached = self.root / 'detached'
self.git('--git-dir', self.bare, 'worktree', 'add', '--detach', detached, 'HEAD')
expected = []
for number, branch in enumerate(['feature/normal', 'issue-fixture']):
checkout = self.root / ('attached-' + str(number))
self.git('--git-dir', self.bare, 'worktree', 'add', '-b', branch, checkout, 'HEAD')
expected.append({'branch': 'refs/heads/' + branch, 'path': str(checkout)})
code, value = self.gate()
self.assertEqual(code, 1)
self.assertEqual(sorted(value['attached_worktrees'], key=lambda x: x['path']), expected)
def test_unavailable_nonbare_and_symlink_input_fail_closed(self):
self.assertEqual(self.gate()[0], 0)
original = self.bare
link = self.root / 'alias'
link.symlink_to(original, target_is_directory=True)
for target in [self.root / 'absent', self.seed / '.git', link]:
with self.subTest(target=str(target)):
self.bare = target
code, value = self.gate()
self.assertEqual(code, 2)
self.assertEqual(value['status'], 'block')
self.bare = original
# Raw Git stderr must not become diagnostic evidence.
(original / 'config').write_text('secret-like fixture invalid config [')
result = subprocess.run(['python3', '-B', str(SCRIPT), '--git-dir', str(original)],
env=self.env, capture_output=True, text=True, timeout=15)
self.assertEqual(result.returncode, 2)
self.assertNotIn('secret-like', result.stdout + result.stderr)
def test_malformed_nul_listing_is_rejected_not_treated_as_empty(self):
from harness.lib import repository_prefetch as module
raw = self.git('--git-dir', self.bare, 'worktree', 'list', '--porcelain', '-z').encode()
self.assertEqual(module.parse_worktrees(raw)[0]['kind'], 'bare')
for bad in [b'', raw[:-1], raw + raw, raw.replace(b'bare', b'unknown'),
b'worktree /x\0HEAD ' + b'a' * 40 + b'\0detached\0\0',
raw + b'worktree /x\0HEAD ' + b'a' * 40 + b'\0branch refs/heads/../bad\0\0']:
with self.subTest(raw=bad):
with self.assertRaises(module.PrefetchError):
module.parse_worktrees(bad)
def test_reader_failure_and_malformed_output_fail_closed_after_valid_control(self):
from harness.lib import repository_prefetch as module
from unittest.mock import patch
self.assertEqual(module.inspect_repository(str(self.bare))['status'], 'pass')
bare = subprocess.CompletedProcess([], 0, b'true\n')
bad = subprocess.CompletedProcess([], 0, b'invalid secret-like fixture')
for responses in ([bare, bad], [bare, None]):
with self.subTest(responses=str(responses)):
with patch.object(module.binding, '_git', side_effect=responses):
with self.assertRaises(module.PrefetchError) as error:
module.inspect_repository(str(self.bare))
self.assertNotIn('secret-like', str(error.exception))
with patch.object(module.binding, '_git', side_effect=module.binding.BindingError('secret-like fixture')):
with self.assertRaisesRegex(module.PrefetchError, '^git_read_unavailable$'):
module.inspect_repository(str(self.bare))
def test_documentation_contract_requires_gate_before_fetch(self):
text = (ROOT / 'harness/operations/repository-cache.md').read_text()
section = text.split('## 기존 cache 갱신', 1)[1].split('\n## ', 1)[0]
self.assertIn('repository_prefetch.py', section)
self.assertLess(section.index('repository_prefetch.py'), section.index('prune/fetch'))
for rule in ['--git-dir', 'repository lock', 'exit 0', 'exit 1', 'exit 2']:
self.assertIn(rule, section)
def fingerprint(self, *roots):
import hashlib
result = {}
for root in roots:
for path in ([root] if root.is_file() else root.rglob('*')):
if path.is_file():
st = path.stat()
result[str(path)] = (hashlib.sha256(path.read_bytes()).hexdigest(),
st.st_mode, st.st_mtime_ns, st.st_ino)
return result
if __name__ == '__main__':
unittest.main()
@@ -0,0 +1,190 @@
from harness.tests._support.paths import HARNESS as SOURCE_HARNESS
import json
import os
import stat
import subprocess
import tempfile
import unittest
from pathlib import Path
from harness.lib.managed_lock import ManagedLockSet
from harness.lib import runtime_root as runtime
class RuntimeRootTests(unittest.TestCase):
def cache(self, root):
repo = root / 'repositories' / 'owner' / 'repo'
for p in [root / 'repositories', root / 'repositories' / 'owner', repo, repo / 'locks']:
p.mkdir(mode=0o700)
return repo
def prepare(self, root, run='workflow-1'):
with ManagedLockSet.for_issue(root, 'owner', 'repo', 17) as locks:
with locks.repository_scope():
return runtime.prepare_issue_runtime(root, 'owner', 'repo', 17, run, lock_set=locks)
def test_new_runtime_is_private_and_preserves_existing_cache(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp); repo = self.cache(root)
os.chmod(root / 'repositories', 0o755)
before = repo.stat()
result = self.prepare(root)
try:
self.assertEqual(result.path, runtime.issue_runtime_path(root, 'owner', 'repo', 17, 'workflow-1'))
self.assertEqual(stat.S_IMODE(os.fstat(result.fd).st_mode), 0o700)
self.assertEqual(os.fstat(result.fd).st_ino, result.path.stat().st_ino)
finally:
os.close(result.fd)
self.assertEqual(repo.stat().st_ino, before.st_ino)
self.assertEqual(stat.S_IMODE((root / 'repositories').stat().st_mode), 0o755)
self.assertFalse((result.path.parents[1] / 'state.json').exists())
def test_missing_or_wrong_lock_set_cannot_create_directories(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp); repo = self.cache(root)
locks = ManagedLockSet.for_issue(root, 'owner', 'repo', 17)
with self.assertRaises(ValueError):
runtime.prepare_issue_runtime(root, 'owner', 'repo', 17, 'run', lock_set=locks)
self.assertFalse((repo / 'worktrees').exists())
with locks:
with self.assertRaises(ValueError):
runtime.prepare_issue_runtime(root, 'owner', 'repo', 17, 'run', lock_set=locks)
with locks.repository_scope():
with self.assertRaises(ValueError):
runtime.prepare_issue_runtime(root, 'owner', 'repo', 18, 'run', lock_set=locks)
self.assertFalse((repo / 'worktrees').exists())
def test_kernel_unlocked_wrappers_cannot_authorize_runtime_creation(self):
import fcntl
for resource in ('issue', 'repository'):
with self.subTest(resource=resource), tempfile.TemporaryDirectory() as tmp:
root = Path(tmp); repo = self.cache(root)
with ManagedLockSet.for_issue(root, 'owner', 'repo', 17) as locks:
with locks.repository_scope():
fcntl.flock(getattr(locks, resource).fd, fcntl.LOCK_UN)
self.assertTrue(getattr(locks, resource).held)
with self.assertRaises(ValueError):
runtime.prepare_issue_runtime(root, 'owner', 'repo', 17, 'run', lock_set=locks)
self.assertFalse((repo / 'worktrees').exists())
def test_run_collision_preserves_competing_run_and_parent_chain(self):
from unittest.mock import patch
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp); self.cache(root)
path = runtime.issue_runtime_path(root, 'owner', 'repo', 17, 'collision')
mkdir = os.mkdir
def competing_run(name, mode=0o777, *, dir_fd=None):
mkdir(name, mode, dir_fd=dir_fd)
if name == 'collision':
raise FileExistsError('competing allocator created run')
with patch.object(runtime.os, 'mkdir', side_effect=competing_run):
with self.assertRaises(ValueError): self.prepare(root, 'collision')
self.assertTrue(path.is_dir())
self.assertTrue(path.parent.is_dir())
self.assertTrue(path.parent.parent.is_dir())
self.assertEqual(list(path.iterdir()), [])
def test_stale_private_run_is_rejected_without_reuse(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp); self.cache(root)
result = self.prepare(root); os.close(result.fd)
marker = result.path / 'marker'; marker.write_text('preserve')
before = result.path.stat()
with self.assertRaises(ValueError): self.prepare(root)
self.assertEqual(result.path.stat().st_ino, before.st_ino)
self.assertEqual(marker.read_text(), 'preserve')
def test_unsafe_parent_is_not_repaired(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp); repo = self.cache(root)
p = repo / 'worktrees'; p.mkdir(mode=0o755)
with self.assertRaises(ValueError): self.prepare(root)
self.assertEqual(stat.S_IMODE(p.stat().st_mode), 0o755)
self.assertEqual(list(p.iterdir()), [])
def test_symlink_parent_cannot_redirect_creation(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp); repo = self.cache(root)
other = root / 'elsewhere'; other.mkdir(mode=0o700)
(repo / 'worktrees').symlink_to(other)
with self.assertRaises((ValueError, OSError)): self.prepare(root)
self.assertEqual(list(other.iterdir()), [])
def test_invalid_identifiers_and_literal_traversal_are_rejected(self):
for root, owner, repository, issue, run in [
('/tmp/../root', 'owner', 'repo', 17, 'run'),
('/tmp/root', '../owner', 'repo', 17, 'run'),
('/tmp/root', 'owner', 'repo', True, 'run'),
('/tmp/root', 'owner', 'repo', 17, '..'),
('/tmp/root', 'owner', 'repo', 17, 'x' * 129),
]:
with self.subTest(root=root, owner=owner, issue=issue, run=run), self.assertRaises(ValueError):
runtime.issue_runtime_path(root, owner, repository, issue, run)
self.assertIn('o' * 255, str(runtime.issue_runtime_path('/tmp/root', 'o' * 255, 'repo', 17, 'run')))
def git(self, *args):
result = subprocess.run(['git', *map(str, args)], capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
return result.stdout
def test_runtime_only_container_supports_real_new_git_checkout_and_removal(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp); repo = self.cache(root)
seed = root / 'seed'
self.git('init', seed)
(seed / 'file').write_text('fixture')
self.git('-C', seed, 'add', 'file')
self.git('-C', seed, '-c', 'user.name=Fixture', '-c', 'user.email=fixture@example.invalid', 'commit', '-m', 'fixture')
self.git('clone', '--bare', seed, repo / 'repository.git')
source = self.git('-C', seed, 'rev-parse', 'HEAD').strip()
run = self.prepare(root); os.close(run.fd)
issue = repo / 'worktrees' / 'issue-17'
self.assertEqual(runtime.inspect_issue_worktree_admission(root, 'owner', 'repo', 17), 'runtime-only')
self.git('--git-dir', repo / 'repository.git', 'worktree', 'add', '--detach', issue / 'checkout', source)
self.assertEqual(runtime.inspect_issue_worktree_admission(root, 'owner', 'repo', 17), 'invalid')
state = issue / 'state.json'; state.write_text(json.dumps({'schema': 'gitea-agent.worktree/v1'})); state.chmod(0o600)
self.assertEqual(runtime.inspect_issue_worktree_admission(root, 'owner', 'repo', 17), 'checkout-candidate')
self.git('--git-dir', repo / 'repository.git', 'worktree', 'remove', issue / 'checkout')
state.unlink()
self.assertEqual(runtime.inspect_issue_worktree_admission(root, 'owner', 'repo', 17), 'runtime-only')
self.assertTrue(run.path.exists())
(issue / 'unknown').write_text('preserve')
self.assertEqual(runtime.inspect_issue_worktree_admission(root, 'owner', 'repo', 17), 'invalid')
def test_inspection_cli_requires_live_repository_lease_and_uses_real_git(self):
import sys
from harness.lib import managed_lock_broker as broker
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp); repo = self.cache(root)
self.git('init', '--bare', repo / 'repository.git')
base = SOURCE_HARNESS
cmd = [sys.executable, '-B', str(base / 'lib' / 'managed_lock_broker.py'), 'serve',
'--root', str(root), '--owner', 'owner', '--repository', 'repo', '--issue', '17',
'--role', 'gitea-impl-agent', '--run-id', 'cli-workflow', '--ttl-seconds', '30']
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
try:
line = process.stdout.readline()
self.assertTrue(line, 'broker failed before ready')
ready = json.loads(line)
inspect = [sys.executable, '-B', str(base / 'lib' / 'runtime_root.py'), 'inspect-worktree',
'--root', str(root), '--owner', 'owner', '--repository', 'repo', '--issue', '17',
'--handle', ready['handle']]
result = subprocess.run(inspect, capture_output=True, text=True, timeout=10)
self.assertNotEqual(result.returncode, 0)
broker.request_handle(ready['handle'], 'acquire_repository')
result = subprocess.run(inspect, capture_output=True, text=True, timeout=10)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(json.loads(result.stdout)['container'], 'runtime-only')
broker.request_handle(ready['handle'], 'release_repository')
broker.request_handle(ready['handle'], 'release')
self.assertEqual(process.wait(timeout=10), 0)
finally:
if process.poll() is None:
process.terminate(); process.wait(timeout=10)
process.stdout.close(); process.stderr.close()
def test_inspection_rejects_missing_cache_instead_of_creating_it(self):
with tempfile.TemporaryDirectory() as tmp:
with self.assertRaises((ValueError, OSError)):
runtime.inspect_issue_worktree_admission(Path(tmp), 'owner', 'repo', 17)
self.assertEqual(list(Path(tmp).iterdir()), [])
@@ -0,0 +1,287 @@
"""Real-Git regressions; no pytest or product execution dependency."""
from harness.tests._support.paths import ROOT as SOURCE_ROOT
import copy
import hashlib
import json
import os
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from harness.lib import test_inventory_binding as binding
ROOT = SOURCE_ROOT
class InventoryBindingTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
self.work = self.root / 'work'
self.work.mkdir()
self.git('init', '-q')
self.git('config', 'user.email', 'fixture@example.invalid')
self.git('config', 'user.name', 'Fixture')
(self.work / 'tests').mkdir()
(self.work / 'tests/sample.py').write_text('def test_sample():\n assert True\n')
(self.work / 'config.ini').write_text('[test]\nvalue=1\n')
self.git('add', '.')
self.git('commit', '-qm', 'baseline')
self.rev = self.git('rev-parse', 'HEAD').strip()
self.gitdir = self.work / '.git'
self.data = self.make_data(self.rev)
def git(self, *args):
return subprocess.check_output(['git', '-C', str(self.work), *args], text=True, stderr=subprocess.DEVNULL)
def make_data(self, revision):
hashes = {p: hashlib.sha256((self.work / p).read_bytes()).hexdigest()
for p in ['config.ini', 'tests/sample.py']}
return {'schema': 'gitea-agent.test-inventory-binding/v1',
'source_revision': revision, 'stage': 'baseline',
'ids': ['tests/sample.py::test_sample'],
'file_sha256': {'tests/sample.py': hashes['tests/sample.py']},
'collection': {'source_revision': revision, 'stage': 'baseline',
'argv': ['python3', '-m', 'pytest', '--collect-only'],
'scope_paths': list(hashes), 'scope_sha256': self.scope_digest(hashes),
'evidence': 'retained-collection-record'}}
@staticmethod
def scope_digest(hashes):
return hashlib.sha256(json.dumps(hashes, sort_keys=True, separators=(',', ':')).encode()).hexdigest()
def verify(self, data=None, revision=None, stage='baseline', gitdir=None):
return binding.verify_inventory(str(gitdir or self.gitdir), self.data if data is None else data,
self.rev if revision is None else revision, stage)
def cli(self, raw=None, extra=(), input_path=None):
inp = self.root / 'input.json'
if input_path is None:
inp.write_text(json.dumps(self.data) if raw is None else raw)
else:
inp = input_path
return subprocess.run([sys.executable, '-B', str(ROOT / 'harness/lib/test_inventory_binding.py'),
'--git-dir', str(self.gitdir), '--input', str(inp),
'--expected-revision', self.rev, '--expected-stage', 'baseline', *extra],
capture_output=True, text=True, timeout=10)
def test_valid_normal_and_bare_without_state_or_ref_changes(self):
before = (self.git('status', '--porcelain'), self.git('show-ref'))
(self.work / 'tests/sample.py').write_text('uncommitted edit')
dirty = self.git('status', '--porcelain')
self.assertEqual(self.verify()['status'], 'pass')
bare = self.root / 'mirror.git'
subprocess.run(['git', 'clone', '-q', '--bare', str(self.work), str(bare)], check=True)
self.assertEqual(self.verify(gitdir=bare)['status'], 'pass')
self.assertEqual(self.git('status', '--porcelain'), dirty)
self.assertEqual(self.git('show-ref'), before[1])
def test_expected_revision_and_stage_cannot_be_self_attested(self):
self.assertEqual(self.verify(revision='0' * 40)['reason_codes'], ['source_revision_mismatch'])
self.assertEqual(self.verify(stage='current')['reason_codes'], ['stage_mismatch'])
newer = copy.deepcopy(self.data)
newer['stage'] = newer['collection']['stage'] = 'current'
self.assertEqual(self.verify(newer)['status'], 'block')
def test_tag_object_is_not_an_exact_commit_revision(self):
self.git('tag', '-a', '-m', 'fixture tag', 'fixture-tag')
tag = self.git('rev-parse', 'fixture-tag').strip()
changed = copy.deepcopy(self.data)
changed['source_revision'] = changed['collection']['source_revision'] = tag
self.assertEqual(self.verify(changed, revision=tag)['status'], 'block')
def test_missing_commit_and_malformed_sha(self):
data = copy.deepcopy(self.data)
data['source_revision'] = data['collection']['source_revision'] = '0' * 40
self.assertEqual(self.verify(data, revision='0' * 40)['reason_codes'], ['revision_not_found'])
for rev in [self.rev.upper(), 'HEAD', self.rev + ' ', True]:
with self.subTest(rev=rev), self.assertRaises(binding.BindingError):
self.verify(revision=rev)
def test_absent_file_and_tampered_recorded_hash_are_blocked(self):
changed = copy.deepcopy(self.data)
changed['file_sha256']['tests/sample.py'] = '0' * 64
self.assertIn('recorded_file_hash_mismatch', self.verify(changed)['reason_codes'])
missing = copy.deepcopy(self.data)
missing['ids'] = ['tests/absent.py::test_absent']
missing['file_sha256'] = {'tests/absent.py': '0' * 64}
missing['collection']['scope_paths'] = ['tests/absent.py']
self.assertIn('scope_file_missing', self.verify(missing)['reason_codes'])
def test_changed_collector_configuration_breaks_scope_digest(self):
(self.work / 'config.ini').write_text('[test]\nvalue=2\n')
self.git('add', 'config.ini')
self.git('commit', '-qm', 'change collection config')
revision = self.git('rev-parse', 'HEAD').strip()
changed = copy.deepcopy(self.data)
changed['source_revision'] = changed['collection']['source_revision'] = revision
self.assertIn('scope_digest_mismatch', self.verify(changed, revision=revision)['reason_codes'])
def test_nested_source_or_stage_and_omitted_scope_are_rejected(self):
for key, value in [('source_revision', '0' * 40), ('stage', 'current'), ('scope_paths', ['config.ini'])]:
changed = copy.deepcopy(self.data)
changed['collection'][key] = value
with self.subTest(key=key), self.assertRaises(binding.BindingError):
self.verify(changed)
def test_duplicates_unsafe_paths_extra_hash_and_wrong_types(self):
variants = []
for value in [self.data['ids'] * 2, [{}], True, ['../outside::case'], ['/outside::case'], ['tests//sample.py::case']]:
changed = copy.deepcopy(self.data); changed['ids'] = value; variants.append(changed)
changed = copy.deepcopy(self.data); changed['extra'] = True; variants.append(changed)
changed = copy.deepcopy(self.data); changed['file_sha256']['extra.py'] = '0' * 64; variants.append(changed)
changed = copy.deepcopy(self.data); changed['collection']['scope_paths'] = [{}]; variants.append(changed)
for changed in variants:
with self.subTest(data_type=type(changed['ids']).__name__), self.assertRaises(binding.BindingError):
self.verify(changed)
def test_empty_collection_is_valid_but_not_a_collection_execution_claim(self):
changed = copy.deepcopy(self.data)
changed['ids'] = []; changed['file_sha256'] = {}
self.assertEqual(self.verify(changed)['status'], 'pass')
changed['collection']['scope_paths'] = []
changed['collection']['scope_sha256'] = self.scope_digest({})
self.assertEqual(self.verify(changed)['status'], 'pass')
def test_symlink_tree_and_gitlink_are_not_regular_blobs(self):
(self.work / 'tests/link.py').symlink_to('sample.py')
self.git('add', 'tests/link.py'); self.git('commit', '-qm', 'add symlink')
self.git('update-index', '--add', '--cacheinfo', f'160000,{self.rev},tests/submodule')
self.git('commit', '-qm', 'add gitlink')
revision = self.git('rev-parse', 'HEAD').strip()
for path in ['tests/link.py', 'tests/submodule', 'tests']:
changed = copy.deepcopy(self.data)
changed['source_revision'] = changed['collection']['source_revision'] = revision
changed['ids'] = [path + '::case']; changed['file_sha256'] = {path: '0' * 64}
changed['collection']['scope_paths'] = [path]
with self.subTest(path=path):
self.assertEqual(self.verify(changed, revision=revision)['status'], 'block')
def test_symlink_git_directory_ancestor_and_lexical_alias_rejected(self):
alias = self.root / 'alias'; alias.symlink_to(self.work, target_is_directory=True)
for path in [str(alias / '.git'), str(self.gitdir) + '/', str(self.work / 'x') + '/../.git']:
with self.subTest(path=path), self.assertRaises(binding.BindingError):
binding.verify_inventory(path, self.data, self.rev, 'baseline')
def test_ambient_git_redirects_and_replacements_do_not_change_binding(self):
self.git('replace', self.rev, self.rev)
with patch.dict(os.environ, {'GIT_DIR': '/nonexistent', 'GIT_WORK_TREE': '/nonexistent',
'GIT_CONFIG_COUNT': '1', 'GIT_CONFIG_KEY_0': 'core.bare',
'GIT_CONFIG_VALUE_0': 'true'}):
self.assertEqual(self.verify()['status'], 'pass')
def test_json_symlink_ancestor_and_lexical_alias_rejected(self):
real = self.root / 'records'
real.mkdir()
record = real / 'record.json'
record.write_text(json.dumps(self.data))
alias = self.root / 'record-alias'
alias.symlink_to(real, target_is_directory=True)
for path in [str(alias / 'record.json'), str(real) + '/./record.json']:
with self.subTest(path=path), self.assertRaises(binding.BindingError):
binding._json_file(path)
def test_lazy_fetch_disabled_in_read_only_git_environment(self):
self.assertEqual(binding._env().get('GIT_NO_LAZY_FETCH'), '1')
def test_json_duplicate_keys_symlink_fifo_and_oversize_rejected(self):
duplicate = json.dumps(self.data)[:-1] + ',"schema":"secret-marker"}'
for raw in [duplicate, '{"secret":"secret-marker"}', '[' * 2000, ' ' * (1024 * 1024 + 1)]:
result = self.cli(raw)
self.assertEqual(result.returncode, 2)
self.assertNotIn('secret-marker', result.stdout + result.stderr)
real = self.root / 'real.json'; real.write_text(json.dumps(self.data))
alias = self.root / 'alias.json'; alias.symlink_to(real)
self.assertEqual(self.cli(input_path=alias).returncode, 2)
fifo = self.root / 'fifo'; os.mkfifo(fifo)
self.assertEqual(self.cli(input_path=fifo).returncode, 2)
def test_cli_pass_block_and_rejected_input_are_json(self):
self.assertEqual(self.cli().returncode, 0)
changed = copy.deepcopy(self.data); changed['stage'] = changed['collection']['stage'] = 'current'
result = self.cli(json.dumps(changed))
self.assertEqual(result.returncode, 1)
self.assertEqual(json.loads(result.stdout)['status'], 'block')
result = self.cli('{"secret":"secret-marker"}')
self.assertEqual(result.returncode, 2)
self.assertNotIn('secret-marker', result.stdout + result.stderr)
def test_recorded_argv_is_not_a_shell_command_or_character_policy(self):
changed = copy.deepcopy(self.data)
changed['collection']['argv'] += ['-k', 'case[param$with|literal;chars]']
try:
result = self.verify(changed)
except binding.BindingError:
self.fail('literal argv must not be rejected as a shell command')
self.assertEqual(result['status'], 'pass')
def test_cli_unknown_option_never_echoes_input(self):
result = self.cli(extra=('--unknown=secret-marker',))
self.assertEqual(result.returncode, 2)
self.assertNotIn('secret-marker', result.stdout + result.stderr)
self.assertTrue(result.stdout.startswith('{'), 'CLI rejection must be bounded JSON')
self.assertEqual(json.loads(result.stdout)['reason_codes'], ['rejected_input'])
def test_git_output_limit_terminates_before_wall_clock_timeout(self):
import time
fake = self.root / 'bin'
fake.mkdir()
program = fake / 'git'
program.write_text('#!' + sys.executable + '\nimport os,time\nos.write(1,b"x"*1024)\ntime.sleep(10)\n')
program.chmod(0o700)
started = time.monotonic()
with patch.object(binding, '_GIT_EXECUTABLE', str(program)), patch.object(binding, '_MAX_OUTPUT', 32):
self.assertIsNone(binding._git(str(self.gitdir), ['fixture-read']))
self.assertLess(time.monotonic() - started, 2.0)
def test_hostile_path_git_is_never_executed(self):
marker = self.root / 'hostile-marker'
fake = self.root / 'bin'
fake.mkdir()
program = fake / 'git'
program.write_text('#!' + sys.executable + '\nimport pathlib\npathlib.Path(' + repr(str(marker)) + ').write_text("executed")\n')
program.chmod(0o700)
with patch.dict(os.environ, {'PATH': str(fake)}):
self.assertEqual(self.verify()['status'], 'pass')
self.assertFalse(marker.exists())
def test_admitted_git_directory_remains_pinned_after_path_replacement(self):
original = binding._git
replacement_work = self.root / 'replacement-work'
replacement_work.mkdir()
subprocess.run(['git', '-C', str(replacement_work), 'init', '-q'], check=True)
subprocess.run(['git', '-C', str(replacement_work), 'config', 'user.email', 'replacement@example.invalid'], check=True)
subprocess.run(['git', '-C', str(replacement_work), 'config', 'user.name', 'Replacement'], check=True)
(replacement_work / 'replacement.txt').write_text('replacement')
subprocess.run(['git', '-C', str(replacement_work), 'add', '.'], check=True)
subprocess.run(['git', '-C', str(replacement_work), 'commit', '-qm', 'replacement'], check=True)
swapped = False
def replace_after_admission(git_dir, args, **kwargs):
nonlocal swapped
result = original(git_dir, args, **kwargs)
if not swapped and args[:2] == ['rev-parse', '--is-bare-repository']:
swapped = True
moved = self.root / 'admitted-original.git'
os.rename(self.gitdir, moved)
os.rename(replacement_work / '.git', self.gitdir)
return result
with patch.object(binding, '_git', side_effect=replace_after_admission):
self.assertEqual(self.verify()['status'], 'pass')
self.assertTrue(swapped)
def test_each_scope_blob_is_read_once(self):
original = binding._blob
with patch.object(binding, '_blob', wraps=original) as read:
self.assertEqual(self.verify()['status'], 'pass')
self.assertEqual(read.call_count, 2)
if __name__ == '__main__':
unittest.main()
@@ -0,0 +1,343 @@
import contextlib
import io
import json
import os
import stat
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from harness.lib import verification_root
class VerificationRootTests(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.root = Path(self.tmp.name) / "cache"
self.issue = self.root / "repositories" / "alice" / "ledger" / "worktrees" / "issue-7"
self.issue.mkdir(parents=True, mode=0o700)
for path in (
self.root / "repositories",
self.root / "repositories/alice",
self.root / "repositories/alice/ledger",
self.root / "repositories/alice/ledger/worktrees",
self.issue,
):
os.chmod(path, 0o700)
# The trusted harness/NFS root is not required to be role-owned 0700.
os.chmod(self.root, 0o770)
self.handle = self.root / "runtime" / "handle.json"
self.handle.parent.mkdir(mode=0o700)
self.meta = {
"root": str(self.root),
"owner": "alice",
"repository": "ledger",
"issue": 7,
"role": "gitea-impl-agent",
}
self.status = {
"status": "held",
"root": str(self.root),
"owner": "alice",
"repository": "ledger",
"issue": 7,
"role": "gitea-impl-agent",
"deadline": 10**20,
}
def tearDown(self):
self.tmp.cleanup()
@contextlib.contextmanager
def broker(self, *, metadata=None, status=None):
with (
patch.object(
verification_root.broker,
"read_handle_metadata",
return_value=self.meta if metadata is None else metadata,
),
patch.object(
verification_root.broker,
"request_handle",
return_value=self.status if status is None else status,
),
):
yield
def test_umask_does_not_make_verification_parent_world_readable(self):
old = os.umask(0o022)
try:
with self.broker():
result = verification_root.prepare(
self.root, "alice", "ledger", 7, "run-1", self.handle
)
finally:
os.umask(old)
verification = self.issue / "verification"
self.assertEqual(result, verification / "run-1")
self.assertEqual(stat.S_IMODE(verification.stat().st_mode), 0o700)
self.assertEqual(stat.S_IMODE((verification / "run-1").stat().st_mode), 0o700)
def test_existing_secure_parent_accepts_a_fresh_run(self):
verification = self.issue / "verification"
verification.mkdir(mode=0o700)
(verification / "prior-run").mkdir(mode=0o700)
with self.broker():
result = verification_root.prepare(
self.root, "alice", "ledger", 7, "new-run", self.handle
)
self.assertEqual(result, verification / "new-run")
self.assertTrue((verification / "prior-run").is_dir())
self.assertEqual(stat.S_IMODE(result.stat().st_mode), 0o700)
def test_bad_lease_is_rejected_before_filesystem_mutation(self):
before = list(self.issue.iterdir())
with self.broker(status={"error": "expired"}):
with self.assertRaises(verification_root.VerificationError):
verification_root.prepare(
self.root, "alice", "ledger", 7, "run-1", self.handle
)
self.assertEqual(list(self.issue.iterdir()), before)
def test_missing_deadline_and_role_mismatch_fail_before_mutation(self):
status = dict(self.status)
status.pop("deadline")
with self.broker(status=status):
with self.assertRaisesRegex(verification_root.VerificationError, "expired"):
verification_root.prepare(
self.root, "alice", "ledger", 7, "run-1", self.handle
)
status = dict(self.status, role="gitea-review-agent")
with self.broker(status=status):
with self.assertRaisesRegex(verification_root.VerificationError, "role"):
verification_root.prepare(
self.root, "alice", "ledger", 7, "run-1", self.handle
)
self.assertFalse((self.issue / "verification").exists())
def test_existing_insecure_verification_is_not_repaired(self):
verification = self.issue / "verification"
verification.mkdir(mode=0o755)
with self.broker():
with self.assertRaises(verification_root.VerificationError):
verification_root.prepare(
self.root, "alice", "ledger", 7, "run-1", self.handle
)
self.assertEqual(stat.S_IMODE(verification.stat().st_mode), 0o755)
self.assertFalse((verification / "run-1").exists())
def test_insecure_issue_directory_is_rejected_without_repair(self):
os.chmod(self.issue, 0o755)
with self.broker():
with self.assertRaisesRegex(verification_root.VerificationError, "Issue directory"):
verification_root.prepare(
self.root, "alice", "ledger", 7, "run-1", self.handle
)
self.assertEqual(stat.S_IMODE(self.issue.stat().st_mode), 0o755)
self.assertFalse((self.issue / "verification").exists())
def test_symlink_and_existing_run_are_rejected_without_mutation(self):
verification = self.issue / "verification"
verification.mkdir(mode=0o700)
existing = verification / "run-1"
existing.mkdir(mode=0o700)
with self.broker():
with self.assertRaisesRegex(verification_root.VerificationError, "existing run"):
verification_root.prepare(
self.root, "alice", "ledger", 7, "run-1", self.handle
)
self.assertTrue(existing.is_dir())
existing.rmdir()
verification.rmdir()
verification.symlink_to(self.issue, target_is_directory=True)
with self.broker():
with self.assertRaises(verification_root.VerificationError):
verification_root.prepare(
self.root, "alice", "ledger", 7, "run-2", self.handle
)
self.assertTrue(verification.is_symlink())
def test_binding_and_identifier_validation(self):
bad = dict(self.meta, owner="mallory")
with self.broker(metadata=bad):
with self.assertRaisesRegex(verification_root.VerificationError, "binding"):
verification_root.prepare(
self.root, "alice", "ledger", 7, "run-1", self.handle
)
wrong_root = dict(self.status, root=str(self.root / "other"))
with self.broker(status=wrong_root):
with self.assertRaisesRegex(verification_root.VerificationError, "binding"):
verification_root.prepare(
self.root, "alice", "ledger", 7, "run-1", self.handle
)
for value in ("../x", "a/b", "", ".", "..", ".hidden", "x" * 129):
with self.subTest(value=value), self.assertRaises(verification_root.VerificationError):
verification_root.prepare(
self.root, "alice", "ledger", 7, value, self.handle
)
def test_real_broker_cli_prepares_canonical_private_root(self):
locks = self.root / "repositories/alice/ledger/locks"
locks.mkdir(mode=0o700)
(locks / "repository.lock").touch(mode=0o600)
broker_script = Path(verification_root.broker.__file__)
self.handle = self.issue / "runtime/broker-run/broker.handle"
socket_path = self.handle.parent / "broker.sock"
command = [
sys.executable,
str(broker_script),
"serve",
"--root",
str(self.root),
"--owner",
"alice",
"--repository",
"ledger",
"--issue",
"7",
"--role",
"gitea-impl-agent",
"--run-id",
"broker-run",
"--handle",
str(self.handle),
"--socket",
str(socket_path),
"--ttl-seconds",
"30",
"--acquire-timeout",
"1",
]
proc = subprocess.Popen(
command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
)
assert proc.stdout is not None and proc.stderr is not None
try:
ready = json.loads(proc.stdout.readline())
self.assertEqual(ready["status"], "held")
helper = subprocess.run(
[
sys.executable,
str(Path(verification_root.__file__)),
"prepare",
"--root",
str(self.root),
"--owner",
"alice",
"--repository",
"ledger",
"--issue",
"7",
"--run-id",
"real-run",
"--handle",
str(self.handle),
],
text=True,
capture_output=True,
)
self.assertEqual(helper.returncode, 0, helper.stderr)
record = json.loads(helper.stdout)
run = Path(record["path"])
self.assertEqual(run, self.issue / "verification/real-run")
self.assertEqual(stat.S_IMODE(run.parent.stat().st_mode), 0o700)
self.assertEqual(stat.S_IMODE(run.stat().st_mode), 0o700)
self.assertEqual(
verification_root.broker.request_handle(self.handle, "release")["status"],
"released",
)
proc.wait(timeout=3)
finally:
if proc.poll() is None:
proc.kill()
proc.wait()
proc.stdout.close()
proc.stderr.close()
def test_created_inode_replacement_fails_without_removing_replacement(self):
verification = self.issue / "verification"
verification.mkdir(mode=0o700)
real_open = verification_root._open_private_child
def replace_before_open(parent_fd, name, label):
os.rename(name, f"{name}.original", src_dir_fd=parent_fd, dst_dir_fd=parent_fd)
os.mkdir(name, 0o700, dir_fd=parent_fd)
return real_open(parent_fd, name, label)
with self.broker(), patch.object(
verification_root, "_open_private_child", side_effect=replace_before_open
):
with self.assertRaisesRegex(verification_root.VerificationError, "changed run directory"):
verification_root.prepare(
self.root, "alice", "ledger", 7, "race-run", self.handle
)
self.assertTrue((verification / "race-run").is_dir())
def test_replaced_new_verification_parent_is_not_removed_on_run_failure(self):
real_create = verification_root._create_private_child
calls = 0
def replace_parent_before_run(parent_fd, name, label):
nonlocal calls
calls += 1
if calls == 1:
return real_create(parent_fd, name, label)
os.rename(
self.issue / "verification",
self.issue / "verification.original",
)
(self.issue / "verification").mkdir(mode=0o700)
raise verification_root.VerificationError("forced run failure")
with self.broker(), patch.object(
verification_root, "_create_private_child", side_effect=replace_parent_before_run
):
with self.assertRaisesRegex(verification_root.VerificationError, "forced run failure"):
verification_root.prepare(
self.root, "alice", "ledger", 7, "race-parent", self.handle
)
self.assertTrue((self.issue / "verification").is_dir())
self.assertTrue((self.issue / "verification.original").is_dir())
def test_cli_is_directly_executable_and_emits_verified_modes(self):
script = Path(verification_root.__file__)
help_result = subprocess.run(
[str(script), "prepare", "--help"],
text=True,
capture_output=True,
)
self.assertEqual(help_result.returncode, 0, help_result.stderr)
for option in ("--root", "--owner", "--repository", "--issue", "--run-id", "--handle"):
self.assertIn(option, help_result.stdout)
output = io.StringIO()
with self.broker(), contextlib.redirect_stdout(output):
code = verification_root.main(
[
"prepare",
"--root",
str(self.root),
"--owner",
"alice",
"--repository",
"ledger",
"--issue",
"7",
"--run-id",
"run-cli",
"--handle",
str(self.handle),
]
)
self.assertEqual(code, 0)
record = json.loads(output.getvalue())
self.assertEqual(record["verification_mode"], "0700")
self.assertEqual(record["run_mode"], "0700")
self.assertEqual(Path(record["path"]), self.issue / "verification/run-cli")
if __name__ == "__main__":
unittest.main()