| 1 | """Independent Oak reader: format re-derivation, tamper detection, bounds.
|
| 2 |
|
| 3 | Two layers:
|
| 4 | - ``SyntheticReaderTests`` author Oak SQLite v1 state directly (pure BLAKE3,
|
| 5 | raw-block zstd frames) so every structural and tamper case runs without the
|
| 6 | Oak binary: a reader that accepts any corrupted row fails here.
|
| 7 | - ``RealOakReaderTests`` read repositories produced by the real Oak CLI (skipped
|
| 8 | when ``oak`` is not installed), including the workflow-evidence path.
|
| 9 | """
|
| 10 | from contextlib import contextmanager
|
| 11 | from unittest import mock
|
| 12 | import hashlib
|
| 13 | import importlib.util
|
| 14 | import json
|
| 15 | import os
|
| 16 | from pathlib import Path
|
| 17 | import shutil
|
| 18 | import sqlite3
|
| 19 | import subprocess
|
| 20 | import sys
|
| 21 | import tempfile
|
| 22 | import unittest
|
| 23 |
|
| 24 | ROOT = Path(__file__).resolve().parents[1]
|
| 25 | READER = ROOT / 'scripts/oakbench/oak_sqlite_reader.py'
|
| 26 | sys.path.insert(0, str(ROOT / 'scripts'))
|
| 27 | from oakbench import oak_sqlite_reader as reader # noqa: E402
|
| 28 | from oakbench import pureblake3, purezstd # noqa: E402
|
| 29 |
|
| 30 | B3 = pureblake3.hexdigest
|
| 31 | SCHEMA = '''
|
| 32 | CREATE TABLE metadata (key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
| 33 | CREATE TABLE schema_migrations (version TEXT PRIMARY KEY, applied_at TEXT, success INTEGER);
|
| 34 | CREATE TABLE blobs (hash TEXT PRIMARY KEY, content BLOB NOT NULL, size INTEGER NOT NULL, codec INTEGER NOT NULL DEFAULT 0);
|
| 35 | CREATE TABLE trees (hash TEXT PRIMARY KEY, created_at TEXT, content BLOB);
|
| 36 | CREATE TABLE tree_entries (tree_hash TEXT NOT NULL, name TEXT NOT NULL, kind TEXT NOT NULL, hash TEXT NOT NULL,
|
| 37 | mode TEXT NOT NULL DEFAULT 'regular', PRIMARY KEY (tree_hash, name));
|
| 38 | CREATE TABLE commits (hash TEXT PRIMARY KEY, branch_name TEXT NOT NULL, parent_hash TEXT, manifest_hash TEXT NOT NULL,
|
| 39 | author TEXT NOT NULL, message TEXT, timestamp TEXT NOT NULL, merge_parent_hash TEXT);
|
| 40 | CREATE TABLE chunks (hash TEXT PRIMARY KEY, content BLOB NOT NULL, size INTEGER NOT NULL);
|
| 41 | CREATE TABLE blob_chunks (blob_hash TEXT NOT NULL, chunk_hash TEXT NOT NULL, chunk_index INTEGER NOT NULL,
|
| 42 | offset INTEGER NOT NULL, size INTEGER NOT NULL, PRIMARY KEY (blob_hash, chunk_index));
|
| 43 | '''
|
| 44 |
|
| 45 |
|
| 46 | class SyntheticRepo:
|
| 47 | """Minimal Oak SQLite v1 writer re-derived from the format, for tests only."""
|
| 48 |
|
| 49 | def __init__(self, root: Path):
|
| 50 | self.root = root
|
| 51 | (root / '.oak').mkdir(parents=True)
|
| 52 | self.path = root / '.oak' / 'oak.db'
|
| 53 | with self.db() as db:
|
| 54 | db.executescript(SCHEMA)
|
| 55 | db.execute("INSERT INTO schema_migrations VALUES ('0014_change_captures','x',1)")
|
| 56 | db.execute("INSERT INTO metadata VALUES ('current_branch','main')")
|
| 57 | self.pin = self.blob(b'pin') # a small verifying blob for the format pin
|
| 58 |
|
| 59 | @contextmanager
|
| 60 | def db(self):
|
| 61 | db = sqlite3.connect(str(self.path))
|
| 62 | try:
|
| 63 | db.execute('PRAGMA journal_mode=DELETE')
|
| 64 | yield db
|
| 65 | db.commit()
|
| 66 | finally:
|
| 67 | db.close()
|
| 68 |
|
| 69 | def blob(self, data: bytes, compressed=True, store=True) -> str:
|
| 70 | digest = B3(data)
|
| 71 | if store:
|
| 72 | content, codec = (purezstd.encode_raw(data), 1) if compressed else (data, 0)
|
| 73 | with self.db() as db:
|
| 74 | db.execute('INSERT OR REPLACE INTO blobs VALUES (?,?,?,?)', (digest, content, len(data), codec))
|
| 75 | return digest
|
| 76 |
|
| 77 | def chunked(self, data: bytes, size=1000, store_blob=False) -> str:
|
| 78 | digest = self.blob(data, store=store_blob)
|
| 79 | with self.db() as db:
|
| 80 | for index, offset in enumerate(range(0, len(data), size)):
|
| 81 | piece = data[offset:offset + size]
|
| 82 | chunk = B3(piece)
|
| 83 | db.execute('INSERT OR REPLACE INTO chunks VALUES (?,?,?)', (chunk, piece, len(piece)))
|
| 84 | db.execute('INSERT INTO blob_chunks VALUES (?,?,?,?,?)', (digest, chunk, index, offset, len(piece)))
|
| 85 | return digest
|
| 86 |
|
| 87 | def tree(self, entries) -> str:
|
| 88 | """entries: iterable of (kind, name, hash, mode); sorted like Oak unless raw."""
|
| 89 | raw = '\n'.join('\t'.join(e) for e in entries).encode()
|
| 90 | digest = B3(raw)
|
| 91 | with self.db() as db:
|
| 92 | db.execute('INSERT OR REPLACE INTO trees(hash,content) VALUES (?,?)', (digest, purezstd.encode_raw(raw)))
|
| 93 | return digest
|
| 94 |
|
| 95 | def commit(self, tree, parent=None, merge=None, timestamp='2026-09-28T10:00:00.123456789+00:00',
|
| 96 | message='m', preimage=None, head=True) -> str:
|
| 97 | fields = ['main', parent or '', merge or '', tree, 'tester', message, timestamp]
|
| 98 | digest = B3(preimage if preimage is not None else '\n'.join(fields).encode())
|
| 99 | with self.db() as db:
|
| 100 | db.execute('INSERT OR REPLACE INTO commits VALUES (?,?,?,?,?,?,?,?)',
|
| 101 | (digest, 'main', parent, tree, 'tester', message, timestamp, merge))
|
| 102 | if head:
|
| 103 | db.execute("INSERT OR REPLACE INTO metadata VALUES ('head',?)", (digest,))
|
| 104 | return digest
|
| 105 |
|
| 106 | def file_tree(self, files: dict) -> str:
|
| 107 | return self.tree(sorted((('blob', name, self.blob(data), 'regular') for name, data in files.items()),
|
| 108 | key=lambda e: e[1]))
|
| 109 |
|
| 110 |
|
| 111 | class SyntheticReaderTests(unittest.TestCase):
|
| 112 | def setUp(self):
|
| 113 | self.tmp = tempfile.TemporaryDirectory()
|
| 114 | self.addCleanup(self.tmp.cleanup)
|
| 115 | self.base = Path(self.tmp.name)
|
| 116 | self.repo = SyntheticRepo(self.base / 'repo')
|
| 117 | self.out = self.base / 'out'
|
| 118 |
|
| 119 | def read(self, **kwargs):
|
| 120 | destination = kwargs.pop('destination', self.out)
|
| 121 | return reader.read_committed_tree(self.repo.root, destination, **kwargs)
|
| 122 |
|
| 123 | def standard(self):
|
| 124 | a = self.repo.blob(b'alpha\n')
|
| 125 | x = self.repo.blob(b'#!/bin/sh\n', compressed=False)
|
| 126 | link = self.repo.blob(b'a.txt')
|
| 127 | sub = self.repo.tree([('blob', 'inner', self.repo.blob(b'inner\n'), 'regular')])
|
| 128 | root = self.repo.tree([('blob', 'a.txt', a, 'regular'), ('blob', 'back\\slash', a, 'regular'),
|
| 129 | ('blob', 'link', link, 'symlink'), ('tree', 'sub', sub, 'tree'),
|
| 130 | ('blob', 'tool', x, 'executable')])
|
| 131 | return self.repo.commit(root)
|
| 132 |
|
| 133 | # --- positive -----------------------------------------------------------
|
| 134 | def test_materializes_files_modes_symlinks_and_manifest(self):
|
| 135 | head = self.standard()
|
| 136 | result = self.read(manifest=True, deep=True, complete=True)
|
| 137 | self.assertEqual(result['head'], head)
|
| 138 | self.assertEqual(result['reader_backend'], 'pure')
|
| 139 | self.assertEqual((self.out / 'a.txt').read_bytes(), b'alpha\n')
|
| 140 | self.assertEqual((self.out / 'back\\slash').read_bytes(), b'alpha\n')
|
| 141 | self.assertTrue(os.access(self.out / 'tool', os.X_OK))
|
| 142 | self.assertEqual(os.readlink(self.out / 'link'), 'a.txt')
|
| 143 | manifest = {e['path']: e for e in result['manifest']}
|
| 144 | self.assertEqual(manifest['link']['target_hex'], b'a.txt'.hex())
|
| 145 | self.assertEqual(manifest['sub']['kind'], 'dir')
|
| 146 | self.assertEqual(manifest['sub/inner']['sha256'], hashlib.sha256(b'inner\n').hexdigest())
|
| 147 | self.assertTrue(manifest['tool']['executable'])
|
| 148 |
|
| 149 | def test_manifest_only_read_writes_nothing(self):
|
| 150 | self.standard()
|
| 151 | result = self.read(destination=None, manifest=True)
|
| 152 | self.assertFalse(self.out.exists())
|
| 153 | self.assertEqual(len(result['manifest']), 6)
|
| 154 |
|
| 155 | def test_scoped_symlink_is_reported_not_materialized(self):
|
| 156 | self.standard()
|
| 157 | result = self.read(scope=['link', 'a.txt'])
|
| 158 | self.assertEqual(result['nonregular_paths'], ['link'])
|
| 159 | self.assertFalse(os.path.lexists(self.out / 'link'))
|
| 160 |
|
| 161 | def test_chunk_only_blob_is_reassembled_and_verified(self):
|
| 162 | data = os.urandom(4500)
|
| 163 | blob = self.repo.chunked(data)
|
| 164 | self.repo.commit(self.repo.tree([('blob', 'big', blob, 'regular')]))
|
| 165 | result = self.read(complete=True)
|
| 166 | self.assertEqual((self.out / 'big').read_bytes(), data)
|
| 167 | self.assertEqual(result['chunked_blobs_reassembled'], 1)
|
| 168 |
|
| 169 | def test_deep_read_crosschecks_chunk_mapping_of_complete_blobs(self):
|
| 170 | blob = self.repo.chunked(os.urandom(2500), store_blob=True)
|
| 171 | self.repo.commit(self.repo.tree([('blob', 'big', blob, 'regular')]))
|
| 172 | self.assertEqual(self.read(deep=True)['chunk_mappings_crosschecked'], 1)
|
| 173 |
|
| 174 | def test_empty_blob_is_implied_by_its_hash(self):
|
| 175 | empty = self.repo.blob(b'', store=False)
|
| 176 | self.repo.commit(self.repo.tree([('blob', 'empty', empty, 'regular')]))
|
| 177 | self.read(complete=True)
|
| 178 | self.assertEqual((self.out / 'empty').read_bytes(), b'')
|
| 179 |
|
| 180 | def test_history_walk_verifies_ancestors_and_reports_shallow_boundary(self):
|
| 181 | first = self.repo.commit(self.repo.file_tree({'f': b'1'}), parent='c' * 64, head=False)
|
| 182 | self.repo.commit(self.repo.file_tree({'f': b'2'}), parent=first)
|
| 183 | history = self.read(deep=True)['history']
|
| 184 | self.assertEqual(history['commits_verified'], 2)
|
| 185 | self.assertEqual(history['missing_parents'], ['c' * 64])
|
| 186 |
|
| 187 | def test_server_microsecond_truncated_timestamp_verifies_like_oak(self):
|
| 188 | tree = self.repo.file_tree({'f': b'x'})
|
| 189 | original = '\n'.join(['main', '', '', tree, 'tester', 'm', '2026-09-28T10:00:00.123456789+00:00']).encode()
|
| 190 | self.repo.commit(tree, timestamp='2026-09-28T10:00:00.123456+00:00', preimage=original)
|
| 191 | self.assertEqual(self.read()['commit_preimage'], 'v1-microsecond-truncated')
|
| 192 |
|
| 193 | def test_legacy_six_field_commit_verifies(self):
|
| 194 | tree = self.repo.file_tree({'f': b'x'})
|
| 195 | legacy = '\n'.join(['main', '', tree, 'tester', 'm', '2026-04-01T00:00:00+00:00']).encode()
|
| 196 | self.repo.commit(tree, timestamp='2026-04-01T00:00:00+00:00', preimage=legacy)
|
| 197 | self.assertEqual(self.read()['commit_preimage'], 'v1-legacy-6-field')
|
| 198 |
|
| 199 | def test_cli_runs_isolated_with_stdlib_only(self):
|
| 200 | self.standard()
|
| 201 | result = subprocess.run([sys.executable, '-I', str(READER), str(self.repo.root), '-', '--manifest', '--deep'],
|
| 202 | capture_output=True, text=True)
|
| 203 | self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
| 204 | payload = json.loads(result.stdout)
|
| 205 | self.assertEqual(payload['reader_version'], reader.READER_VERSION)
|
| 206 | self.assertEqual(payload['codec_sha256'], reader.codec_sha256())
|
| 207 |
|
| 208 | # --- tamper detection: every corrupted row is flagged Invalid ----------
|
| 209 | def assert_invalid(self, reason, **kwargs):
|
| 210 | with self.assertRaises(reader.Invalid) as caught:
|
| 211 | self.read(**kwargs)
|
| 212 | self.assertIn(reason, str(caught.exception))
|
| 213 |
|
| 214 | def test_tampered_blob_bytes(self):
|
| 215 | self.standard()
|
| 216 | target = self.repo.blob(b'alpha\n')
|
| 217 | with self.repo.db() as db:
|
| 218 | db.execute('UPDATE blobs SET content=? WHERE hash=?', (purezstd.encode_raw(b'alphb\n'), target))
|
| 219 | self.assert_invalid('hash mismatch')
|
| 220 |
|
| 221 | def test_tampered_blob_compressed_frame(self):
|
| 222 | self.standard()
|
| 223 | target = self.repo.blob(b'alpha\n')
|
| 224 | with self.repo.db() as db:
|
| 225 | frame = bytearray(db.execute('SELECT content FROM blobs WHERE hash=?', (target,)).fetchone()[0])
|
| 226 | frame[-6] ^= 1 # payload byte of a raw block; the XXH64 checksum no longer matches
|
| 227 | db.execute('UPDATE blobs SET content=? WHERE hash=?', (bytes(frame), target))
|
| 228 | self.assert_invalid('invalid compressed object')
|
| 229 |
|
| 230 | def test_tampered_blob_size(self):
|
| 231 | self.standard()
|
| 232 | with self.repo.db() as db:
|
| 233 | db.execute('UPDATE blobs SET size=size+1 WHERE hash=?', (self.repo.blob(b'alpha\n'),))
|
| 234 | self.assert_invalid('size mismatch')
|
| 235 |
|
| 236 | def test_tampered_nested_tree_row(self):
|
| 237 | self.standard()
|
| 238 | sub = self.repo.tree([('blob', 'inner', self.repo.blob(b'inner\n'), 'regular')])
|
| 239 | with self.repo.db() as db:
|
| 240 | db.execute('UPDATE trees SET content=? WHERE hash=?',
|
| 241 | (purezstd.encode_raw(b'blob\tinner\t' + b'0' * 64 + b'\tregular'), sub))
|
| 242 | self.assert_invalid('hash mismatch')
|
| 243 |
|
| 244 | def test_missing_nested_tree_row(self):
|
| 245 | self.standard()
|
| 246 | sub = self.repo.tree([('blob', 'inner', self.repo.blob(b'inner\n'), 'regular')])
|
| 247 | with self.repo.db() as db:
|
| 248 | db.execute('DELETE FROM trees WHERE hash=?', (sub,))
|
| 249 | self.assert_invalid('tree missing')
|
| 250 |
|
| 251 | def test_tampered_root_tree_is_unrecognized_format(self):
|
| 252 | self.standard()
|
| 253 | with self.repo.db() as db:
|
| 254 | head = db.execute("SELECT manifest_hash FROM commits").fetchone()[0]
|
| 255 | db.execute('UPDATE trees SET content=? WHERE hash=?', (purezstd.encode_raw(b'x'), head))
|
| 256 | with self.assertRaisesRegex(reader.Unavailable, 'root tree serialization unrecognized'):
|
| 257 | self.read()
|
| 258 |
|
| 259 | def test_tampered_head_commit_is_unrecognized_format(self):
|
| 260 | self.standard()
|
| 261 | with self.repo.db() as db:
|
| 262 | db.execute("UPDATE commits SET message='tampered'")
|
| 263 | with self.assertRaisesRegex(reader.Unavailable, 'commit preimage'):
|
| 264 | self.read()
|
| 265 |
|
| 266 | def test_tampered_ancestor_commit(self):
|
| 267 | first = self.repo.commit(self.repo.file_tree({'f': b'1'}), head=False)
|
| 268 | self.repo.commit(self.repo.file_tree({'f': b'2'}), parent=first)
|
| 269 | with self.repo.db() as db:
|
| 270 | db.execute("UPDATE commits SET author='mallory' WHERE hash=?", (first,))
|
| 271 | self.assert_invalid('ancestor commit hash mismatch', deep=True)
|
| 272 |
|
| 273 | def test_tampered_chunk_content(self):
|
| 274 | blob = self.repo.chunked(os.urandom(2500))
|
| 275 | self.repo.commit(self.repo.tree([('blob', 'big', blob, 'regular')]))
|
| 276 | with self.repo.db() as db:
|
| 277 | db.execute('UPDATE chunks SET content=? WHERE size=500', (os.urandom(500),))
|
| 278 | self.assert_invalid('hash mismatch')
|
| 279 |
|
| 280 | def test_chunk_mapping_gap(self):
|
| 281 | blob = self.repo.chunked(os.urandom(2500))
|
| 282 | self.repo.commit(self.repo.tree([('blob', 'big', blob, 'regular')]))
|
| 283 | with self.repo.db() as db:
|
| 284 | db.execute('UPDATE blob_chunks SET offset=offset+1 WHERE chunk_index=1')
|
| 285 | self.assert_invalid('non-contiguous')
|
| 286 |
|
| 287 | def test_chunk_map_disagreeing_with_complete_blob_in_deep_mode(self):
|
| 288 | blob = self.repo.chunked(os.urandom(2500), store_blob=True)
|
| 289 | self.repo.commit(self.repo.tree([('blob', 'big', blob, 'regular')]))
|
| 290 | with self.repo.db() as db:
|
| 291 | db.execute('DELETE FROM blob_chunks WHERE chunk_index=2')
|
| 292 | self.read() # a shallow read trusts the verified complete blob
|
| 293 | shutil.rmtree(self.out)
|
| 294 | self.assert_invalid('size mismatch', deep=True)
|
| 295 |
|
| 296 | def test_absent_content_is_invalid_when_complete_required(self):
|
| 297 | blob = self.repo.chunked(os.urandom(2500))
|
| 298 | self.repo.commit(self.repo.tree([('blob', 'big', blob, 'regular')]))
|
| 299 | with self.repo.db() as db:
|
| 300 | db.execute('DELETE FROM chunks WHERE size=500')
|
| 301 | with self.assertRaisesRegex(reader.Unavailable, 'chunk content absent'):
|
| 302 | self.read()
|
| 303 | self.assert_invalid('chunk content absent', destination=None, complete=True)
|
| 304 |
|
| 305 | def test_unhydrated_blob_is_unavailable_unless_complete(self):
|
| 306 | ghost = self.repo.blob(b'never stored', store=False)
|
| 307 | self.repo.commit(self.repo.tree([('blob', 'ghost', ghost, 'regular')]))
|
| 308 | with self.assertRaisesRegex(reader.Unavailable, 'no chunk mapping'):
|
| 309 | self.read()
|
| 310 | self.assert_invalid('no chunk mapping', destination=None, complete=True)
|
| 311 |
|
| 312 | def test_structural_violations(self):
|
| 313 | h = self.repo.pin
|
| 314 | cases = {
|
| 315 | 'control character': [('blob', 'tab\tx', h, 'regular')],
|
| 316 | 'reserved name': [('blob', '..', h, 'regular')],
|
| 317 | 'VCS metadata': [('blob', '.oak', h, 'regular')],
|
| 318 | 'order': [('blob', 'b', h, 'regular'), ('blob', 'a', h, 'regular')],
|
| 319 | 'invalid object hash in tree entry': [('blob', 'a', 'A' * 64, 'regular')],
|
| 320 | }
|
| 321 | for reason, entries in cases.items():
|
| 322 | with self.subTest(reason=reason):
|
| 323 | # The tab case cannot even be serialized as 4 fields; either way Invalid.
|
| 324 | self.repo.commit(self.repo.tree(entries))
|
| 325 | with self.assertRaises(reader.Invalid):
|
| 326 | self.read(destination=None)
|
| 327 |
|
| 328 | def test_unknown_kind_or_mode_is_unavailable_unscoped_and_reported_scoped(self):
|
| 329 | self.repo.commit(self.repo.tree([('blob', 'a', self.repo.pin, 'socket'),
|
| 330 | ('blob', 'b', self.repo.pin, 'regular')]))
|
| 331 | with self.assertRaisesRegex(reader.Unavailable, 'unsupported tree entry'):
|
| 332 | self.read(destination=None)
|
| 333 | self.assertEqual(self.read(scope=['a', 'b'])['nonregular_paths'], ['a'])
|
| 334 |
|
| 335 | def test_empty_symlink_target_is_invalid(self):
|
| 336 | empty = self.repo.blob(b'', store=False)
|
| 337 | self.repo.commit(self.repo.tree([('blob', 'l', empty, 'symlink')]))
|
| 338 | self.assert_invalid('invalid symlink target', destination=None)
|
| 339 |
|
| 340 | def test_symlinks_are_created_after_all_regular_writes(self):
|
| 341 | # A link 'a' -> outside and a tree 'b' cannot route a write through the link.
|
| 342 | outside = self.base / 'outside'
|
| 343 | outside.mkdir()
|
| 344 | link = self.repo.blob(str(outside).encode())
|
| 345 | sub = self.repo.tree([('blob', 'f', self.repo.blob(b'inside\n'), 'regular')])
|
| 346 | self.repo.commit(self.repo.tree([('blob', 'a', link, 'symlink'), ('tree', 'b', sub, 'tree')]))
|
| 347 | self.read()
|
| 348 | self.assertEqual(list(outside.iterdir()), [])
|
| 349 | self.assertEqual((self.out / 'b' / 'f').read_bytes(), b'inside\n')
|
| 350 |
|
| 351 | # --- format pins and bounds (unmeasured, never Oak failures) -----------
|
| 352 | def test_newer_migration_and_hash_format_are_unavailable(self):
|
| 353 | self.standard()
|
| 354 | with self.repo.db() as db:
|
| 355 | db.execute("INSERT INTO metadata VALUES ('hash_format','v2')")
|
| 356 | with self.assertRaisesRegex(reader.Unavailable, 'hash format'):
|
| 357 | self.read(destination=None)
|
| 358 | with self.repo.db() as db:
|
| 359 | db.execute("DELETE FROM metadata WHERE key='hash_format'")
|
| 360 | db.execute("INSERT INTO schema_migrations VALUES ('9999_future','x',1)")
|
| 361 | with self.assertRaisesRegex(reader.Unavailable, 'schema migration'):
|
| 362 | self.read(destination=None)
|
| 363 |
|
| 364 | def test_declared_window_over_bound_is_unavailable_without_allocation(self):
|
| 365 | self.standard()
|
| 366 | frame = b'\x28\xb5\x2f\xfd\xa0' + (reader.MAX_OBJECT + 1).to_bytes(4, 'little') + b'\x01\x00\x00'
|
| 367 | with self.repo.db() as db:
|
| 368 | db.execute('UPDATE blobs SET content=?,codec=1 WHERE hash=?', (frame, self.repo.blob(b'alpha\n')))
|
| 369 | with self.assertRaisesRegex(reader.Unavailable, 'window exceeds'):
|
| 370 | self.read(destination=None)
|
| 371 |
|
| 372 | def test_bound_limited_sample_is_not_reported_as_format_drift(self):
|
| 373 | # A repository whose only blobs exceed reader bounds (e.g. a 1 GiB
|
| 374 | # sparse file that compresses tiny) is a bound, not a format change.
|
| 375 | with self.repo.db() as db:
|
| 376 | db.execute('DELETE FROM blobs')
|
| 377 | huge = self.repo.blob(b'z', store=False)
|
| 378 | frame = b'\x28\xb5\x2f\xfd\xa0' + (reader.MAX_OBJECT + 1).to_bytes(4, 'little') + b'\x01\x00\x00'
|
| 379 | with self.repo.db() as db:
|
| 380 | db.execute('INSERT INTO blobs VALUES (?,?,?,1)', (huge, frame, reader.MAX_OBJECT + 1))
|
| 381 | self.repo.commit(self.repo.tree([('blob', 'huge', huge, 'regular')]))
|
| 382 | with self.assertRaises(reader.Unavailable) as caught:
|
| 383 | self.read(destination=None)
|
| 384 | self.assertNotIn('format unrecognized', str(caught.exception))
|
| 385 |
|
| 386 | def test_unknown_codec_and_missing_database(self):
|
| 387 | self.standard()
|
| 388 | with self.repo.db() as db:
|
| 389 | db.execute('UPDATE blobs SET codec=9 WHERE hash=?', (self.repo.blob(b'alpha\n'),))
|
| 390 | with self.assertRaisesRegex(reader.Unavailable, 'unsupported object codec'):
|
| 391 | self.read(destination=None)
|
| 392 | shutil.rmtree(self.repo.root / '.oak')
|
| 393 | self.assert_invalid('database missing', destination=None)
|
| 394 |
|
| 395 | def test_active_wal_is_unavailable(self):
|
| 396 | self.standard()
|
| 397 | Path(str(self.repo.path) + '-wal').write_bytes(b'x')
|
| 398 | with self.assertRaisesRegex(reader.Unavailable, 'active SQLite journal'):
|
| 399 | self.read(destination=None)
|
| 400 |
|
| 401 | def test_native_backend_requires_optional_packages(self):
|
| 402 | self.standard()
|
| 403 | if importlib.util.find_spec('blake3') and importlib.util.find_spec('zstandard'):
|
| 404 | self.assertEqual(self.read(destination=None, backend='native')['reader_backend'], 'native')
|
| 405 | else:
|
| 406 | with self.assertRaisesRegex(reader.Unavailable, 'native reader backend'):
|
| 407 | self.read(destination=None, backend='native')
|
| 408 |
|
| 409 | def test_database_change_during_read_is_unavailable(self):
|
| 410 | self.standard()
|
| 411 | original = Path.write_bytes
|
| 412 |
|
| 413 | def write_then_change(path, content):
|
| 414 | result = original(path, content)
|
| 415 | if path.name == 'snapshot.db':
|
| 416 | with self.repo.db() as db:
|
| 417 | db.execute("INSERT OR REPLACE INTO metadata VALUES ('changed','1')")
|
| 418 | return result
|
| 419 | with mock.patch.object(Path, 'write_bytes', write_then_change):
|
| 420 | with self.assertRaisesRegex(reader.Unavailable, 'database changed'):
|
| 421 | self.read(destination=None)
|
| 422 |
|
| 423 |
|
| 424 | @unittest.skipUnless(shutil.which('oak'), 'Oak binary required')
|
| 425 | class RealOakReaderTests(unittest.TestCase):
|
| 426 | def setUp(self):
|
| 427 | self.tmp = tempfile.TemporaryDirectory()
|
| 428 | self.addCleanup(self.tmp.cleanup)
|
| 429 | self.root = Path(self.tmp.name)
|
| 430 | self.repo = self.root / 'repo'
|
| 431 | self.repo.mkdir()
|
| 432 | self.env = {**os.environ, 'OAK_NO_UPDATE_CHECK': '1', 'HOME': str(self.root)}
|
| 433 | self.oak('init')
|
| 434 | (self.repo / 'payload.txt').write_text('committed payload\n')
|
| 435 | (self.repo / 'pin.txt').write_text('pin\n')
|
| 436 | self.oak('commit', '--no-verify')
|
| 437 |
|
| 438 | def oak(self, *args):
|
| 439 | subprocess.run([shutil.which('oak'), *args], cwd=self.repo, env=self.env, check=True, capture_output=True)
|
| 440 |
|
| 441 | @contextmanager
|
| 442 | def database(self):
|
| 443 | db = sqlite3.connect(str(self.repo / '.oak/oak.db'))
|
| 444 | try:
|
| 445 | db.execute('PRAGMA journal_mode=DELETE')
|
| 446 | yield db
|
| 447 | db.commit()
|
| 448 | finally:
|
| 449 | db.close()
|
| 450 |
|
| 451 | def test_reads_committed_content_not_worktree(self):
|
| 452 | (self.repo / 'payload.txt').write_text('uncommitted replacement')
|
| 453 | result = reader.read_committed_tree(self.repo, self.root / 'out', deep=True, complete=True)
|
| 454 | self.assertEqual((self.root / 'out/payload.txt').read_text(), 'committed payload\n')
|
| 455 | self.assertEqual(result['commit_preimage'], 'v1')
|
| 456 |
|
| 457 | def test_manifest_matches_filesystem_for_modes_and_symlinks(self):
|
| 458 | (self.repo / 'tool.sh').write_text('#!/bin/sh\n')
|
| 459 | (self.repo / 'tool.sh').chmod(0o755)
|
| 460 | (self.repo / 'link').symlink_to('payload.txt')
|
| 461 | (self.repo / 'nested dir').mkdir()
|
| 462 | (self.repo / 'nested dir' / 'caf\u00e9').write_text('x\n')
|
| 463 | self.oak('commit', '--no-verify')
|
| 464 | from oakbench.conformance_matrix import fs_manifest, reader_projection
|
| 465 | result = reader.read_committed_tree(self.repo, None, manifest=True, deep=True, complete=True)
|
| 466 | self.assertEqual(reader_projection(result['manifest']), fs_manifest(self.repo))
|
| 467 |
|
| 468 | def test_corrupt_real_blob_is_invalid(self):
|
| 469 | with self.database() as db:
|
| 470 | db.execute('UPDATE blobs SET content=?, size=?, codec=0 WHERE size=?', (b'corrupt', 7, len(b'committed payload\n')))
|
| 471 | with self.assertRaisesRegex(reader.Invalid, 'hash mismatch'):
|
| 472 | reader.read_committed_tree(self.repo, None)
|
| 473 |
|
| 474 | def test_cli_exit_codes(self):
|
| 475 | ok = subprocess.run([sys.executable, '-I', str(READER), str(self.repo), '-'], capture_output=True, text=True)
|
| 476 | self.assertEqual(ok.returncode, 0, ok.stdout + ok.stderr)
|
| 477 | with self.database() as db:
|
| 478 | db.execute("DELETE FROM metadata WHERE key='head'")
|
| 479 | bad = subprocess.run([sys.executable, '-I', str(READER), str(self.repo), '-'], capture_output=True, text=True)
|
| 480 | self.assertEqual(bad.returncode, 1)
|
| 481 | self.assertTrue(json.loads(bad.stdout)['corrupt'])
|
| 482 |
|
| 483 | def commit_bugfix_payload(self):
|
| 484 | (self.repo / 'app').mkdir()
|
| 485 | (self.repo / 'app/pricing.py').write_text('def f(cents, percent):\n return cents - ((cents * percent) // 100)\n')
|
| 486 | self.oak('commit', '--no-verify')
|
| 487 |
|
| 488 | def evidence(self):
|
| 489 | from oakbench.workflow_oracles import independent_oak_evidence, VERSION
|
| 490 | return independent_oak_evidence('bugfix_test_loop', self.repo, sys.executable,
|
| 491 | {'oracle_version': VERSION, 'workflow_integrity_source': 'trusted_task_worktree',
|
| 492 | 'workflow_integrity_passed': None, 'workflow_integrity_unmeasured_reason': None})
|
| 493 |
|
| 494 | def test_workflow_evidence_is_independent_by_default(self):
|
| 495 | self.commit_bugfix_payload()
|
| 496 | result = self.evidence()
|
| 497 | self.assertIs(result['workflow_integrity_passed'], True, result)
|
| 498 | self.assertEqual(result['oak_reader_backend'], 'pure')
|
| 499 | self.assertEqual(result['workflow_integrity_committed_scope'], ['app/pricing.py'])
|
| 500 |
|
| 501 | def test_in_scope_blob_over_task_limit_fails_like_git(self):
|
| 502 | self.commit_bugfix_payload()
|
| 503 | with self.database() as db:
|
| 504 | db.execute('UPDATE blobs SET size=? WHERE size>?', (32 * 1024 * 1024 + 1, len(b'committed payload\n')))
|
| 505 | result = self.evidence()
|
| 506 | self.assertIs(result['workflow_integrity_passed'], False, result)
|
| 507 | self.assertEqual(result['workflow_integrity_failure_reason'], 'committed_payload_over_limit:app/pricing.py')
|
| 508 |
|
| 509 | def test_corrupt_reader_envelope_reaches_workflow_failure(self):
|
| 510 | self.commit_bugfix_payload()
|
| 511 | with self.database() as db:
|
| 512 | db.execute('UPDATE blobs SET content=?,size=?,codec=0 WHERE size>?', (b'corrupt', 7, len(b'committed payload\n')))
|
| 513 | result = self.evidence()
|
| 514 | self.assertIs(result['workflow_integrity_passed'], False, result)
|
| 515 | self.assertIn('oak_stored_content_invalid', result['workflow_integrity_failure_reason'])
|
| 516 | self.assertIsNone(result['workflow_integrity_unmeasured_reason'])
|
| 517 |
|
| 518 |
|
| 519 | if __name__ == '__main__':
|
| 520 | unittest.main()
|