| 1 | """Positive CLI checks and fault-injected negative oracle controls."""
|
| 2 | import os
|
| 3 | from pathlib import Path
|
| 4 | import shutil
|
| 5 | import subprocess
|
| 6 | import sys
|
| 7 | import tempfile
|
| 8 | import unittest
|
| 9 | from unittest.mock import patch
|
| 10 |
|
| 11 | sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
|
| 12 | from oakbench.conformance import (Repository, SCENARIOS, assert_state,
|
| 13 | run_scenario, tree_manifest)
|
| 14 |
|
| 15 |
|
| 16 | class ManifestTests(unittest.TestCase):
|
| 17 | def test_exact_oracle_rejects_each_loss_class(self):
|
| 18 | with tempfile.TemporaryDirectory() as tmp:
|
| 19 | root = Path(tmp)
|
| 20 | target = root / "payload"
|
| 21 | target.write_bytes(b"sentinel\x00\xff")
|
| 22 | expected = tree_manifest(root)
|
| 23 | target.write_bytes(b"sentinel\x00\xfe")
|
| 24 | with self.assertRaises(AssertionError):
|
| 25 | assert_state(expected, tree_manifest(root))
|
| 26 | target.write_bytes(b"sentinel\x00\xff")
|
| 27 | target.rename(root / "renamed")
|
| 28 | with self.assertRaises(AssertionError):
|
| 29 | assert_state(expected, tree_manifest(root))
|
| 30 | (root / "renamed").rename(target)
|
| 31 | (root / "extra").write_bytes(b"")
|
| 32 | with self.assertRaises(AssertionError):
|
| 33 | assert_state(expected, tree_manifest(root))
|
| 34 | (root / "extra").unlink()
|
| 35 | if os.name == "posix":
|
| 36 | target.chmod(0o755)
|
| 37 | with self.assertRaises(AssertionError):
|
| 38 | assert_state(expected, tree_manifest(root))
|
| 39 |
|
| 40 | @unittest.skipUnless(os.name == "posix", "POSIX symlinks required")
|
| 41 | def test_symlink_target_type_and_no_traversal(self):
|
| 42 | with tempfile.TemporaryDirectory() as tmp:
|
| 43 | root = Path(tmp)
|
| 44 | (root / "link").symlink_to("missing")
|
| 45 | expected = tree_manifest(root)
|
| 46 | (root / "link").unlink()
|
| 47 | (root / "link").symlink_to("different")
|
| 48 | with self.assertRaises(AssertionError):
|
| 49 | assert_state(expected, tree_manifest(root))
|
| 50 | (root / "link").unlink()
|
| 51 | (root / "link").write_text("missing")
|
| 52 | with self.assertRaises(AssertionError):
|
| 53 | assert_state(expected, tree_manifest(root))
|
| 54 | (root / "cycle").symlink_to(".")
|
| 55 | self.assertEqual(tree_manifest(root)["cycle"], {"kind": "symlink", "target": "."})
|
| 56 |
|
| 57 | def test_missing_binary_is_explicit_unmeasured_skip(self):
|
| 58 | with tempfile.TemporaryDirectory() as tmp:
|
| 59 | row = run_scenario(Path(tmp), "oak", "/nonexistent/conformance-oak", SCENARIOS[0])
|
| 60 | self.assertEqual(row["returncode"], 77)
|
| 61 | self.assertIsNone(row["integrity_ok"])
|
| 62 | self.assertIsNone(row["wall_ms"])
|
| 63 | self.assertTrue(row["skip_reason"])
|
| 64 |
|
| 65 |
|
| 66 | @unittest.skipUnless(shutil.which("git"), "Git binary required")
|
| 67 | class RealGitControls(unittest.TestCase):
|
| 68 | @unittest.skipUnless(os.name == "posix", "POSIX symlinks required")
|
| 69 | def test_relative_binary_resolved_before_fixture_cwd_change(self):
|
| 70 | with tempfile.TemporaryDirectory() as tmp:
|
| 71 | root = Path(tmp)
|
| 72 | (root / "git-subject").symlink_to(shutil.which("git"))
|
| 73 | relative_binary = os.path.relpath(root / "git-subject")
|
| 74 | row = run_scenario(root / "work", "git", relative_binary, "byte_roundtrip_v1")
|
| 75 | self.assertEqual(row["returncode"], 0, row.get("failure"))
|
| 76 | self.assertTrue(Path(row["binary"]).is_absolute())
|
| 77 |
|
| 78 | def test_five_positive_scenarios(self):
|
| 79 | with tempfile.TemporaryDirectory() as tmp:
|
| 80 | for scenario in SCENARIOS:
|
| 81 | with self.subTest(scenario=scenario):
|
| 82 | row = run_scenario(Path(tmp), "git", "git", scenario)
|
| 83 | if scenario == "mode_symlink_roundtrip_v1" and row["returncode"] == 77:
|
| 84 | self.assertTrue(row["skip_reason"])
|
| 85 | else:
|
| 86 | self.assertEqual(row["returncode"], 0, row.get("failure"))
|
| 87 | self.assertTrue(row["integrity_ok"])
|
| 88 |
|
| 89 | def test_noop_restore_cannot_pass(self):
|
| 90 | with tempfile.TemporaryDirectory() as tmp, patch.object(Repository, "restore"):
|
| 91 | row = run_scenario(Path(tmp), "git", "git", "byte_roundtrip_v1")
|
| 92 | self.assertEqual(row["returncode"], 1)
|
| 93 | self.assertFalse(row["integrity_ok"])
|
| 94 |
|
| 95 | def test_restore_cannot_erase_unrelated_work(self):
|
| 96 | real_restore = Repository.restore
|
| 97 | def lossy_restore(repo, paths, invalid_source=False):
|
| 98 | result = real_restore(repo, paths, invalid_source)
|
| 99 | (repo.root / "untracked.txt").unlink()
|
| 100 | return result
|
| 101 | with tempfile.TemporaryDirectory() as tmp, patch.object(Repository, "restore", lossy_restore):
|
| 102 | row = run_scenario(Path(tmp), "git", "git", "scoped_restore_preservation_v1")
|
| 103 | self.assertEqual(row["returncode"], 1)
|
| 104 | self.assertIn("untracked.txt", row["failure"])
|
| 105 |
|
| 106 | def test_expected_failure_cannot_hide_partial_write_or_success(self):
|
| 107 | def partial_failure(repo, paths, invalid_source=False):
|
| 108 | (repo.root / "other.txt").write_bytes(b"corrupted")
|
| 109 | return subprocess.CompletedProcess([], 1)
|
| 110 | for implementation in (partial_failure,
|
| 111 | lambda *a, **kw: subprocess.CompletedProcess([], 0),
|
| 112 | lambda *a, **kw: subprocess.CompletedProcess([], -9)):
|
| 113 | with self.subTest(implementation=implementation):
|
| 114 | with tempfile.TemporaryDirectory() as tmp, patch.object(Repository, "restore", implementation):
|
| 115 | row = run_scenario(Path(tmp), "git", "git", "failed_restore_preservation_v1")
|
| 116 | self.assertEqual(row["returncode"], 1)
|
| 117 | self.assertFalse(row["integrity_ok"])
|
| 118 |
|
| 119 | def test_expected_failure_cannot_move_head_without_changing_tree(self):
|
| 120 | def move_head(repo, paths, invalid_source=False):
|
| 121 | repo.run("commit", "--allow-empty", "-m", "unexpected state mutation")
|
| 122 | return subprocess.CompletedProcess([], 1)
|
| 123 | with tempfile.TemporaryDirectory() as tmp, patch.object(Repository, "restore", move_head):
|
| 124 | row = run_scenario(Path(tmp), "git", "git", "failed_restore_preservation_v1")
|
| 125 | self.assertEqual(row["returncode"], 1)
|
| 126 | self.assertIn("head", row["failure"])
|
| 127 |
|
| 128 |
|
| 129 | if __name__ == "__main__":
|
| 130 | unittest.main()
|