| """Sandbox tests: VFS, shell, puzzles, fabrication, mirror, and the |
| no-real-filesystem tripwire.""" |
|
|
| from pathlib import Path |
|
|
| import pytest |
|
|
| from scrypt.sandbox.fabricate import fabricate_home |
| from scrypt.sandbox.mirror import mirror_home |
| from scrypt.sandbox.puzzles import plant_all |
| from scrypt.sandbox.shell import Shell |
| from scrypt.sandbox.vfs import VFS, VfsError |
|
|
|
|
| def make_shell(seed: int = 1) -> Shell: |
| vfs = VFS() |
| fabricate_home(vfs, seed=seed) |
| return Shell(vfs) |
|
|
|
|
| |
|
|
| def test_vfs_write_read_roundtrip(): |
| vfs = VFS() |
| vfs.write("/home/drifter/a/b.txt", "hello") |
| assert vfs.read("/home/drifter/a/b.txt") == "hello" |
| assert vfs.read("~/a/b.txt") == "hello" |
|
|
|
|
| def test_vfs_relative_paths_and_dotdot(): |
| vfs = VFS() |
| vfs.write("~/x/deep/file.txt", "v") |
| vfs.chdir("~/x/deep") |
| assert vfs.read("file.txt") == "v" |
| assert vfs.read("../deep/file.txt") == "v" |
| vfs.chdir("..") |
| assert vfs.cwd_path == "/home/drifter/x" |
|
|
|
|
| def test_vfs_remove_counts_files(): |
| vfs = VFS() |
| for i in range(5): |
| vfs.write(f"~/photos/{i}.jpg", "x") |
| assert vfs.remove("~/photos", recursive=True) == 5 |
| assert vfs.resolve("~/photos") is None |
| with pytest.raises(VfsError): |
| vfs.remove("~/photos") |
|
|
|
|
| |
|
|
| def test_shell_ls_hides_dotfiles_without_dash_a(): |
| sh = make_shell() |
| plain = sh.run("ls").out |
| assert ".bash_history" not in plain |
| assert ".bash_history" in sh.run("ls -a").out |
|
|
|
|
| def test_shell_pipe_cat_grep(): |
| sh = make_shell() |
| out = sh.run("cat documents/todo.txt | grep insurance").out |
| assert "insurance" in out and "gym" not in out |
|
|
|
|
| def test_shell_grep_file_directly(): |
| sh = make_shell() |
| assert "insurance" in sh.run("grep insurance documents/todo.txt").out |
|
|
|
|
| def test_shell_glob_expansion(): |
| sh = make_shell() |
| sh.vfs.chdir("~/photos/vacation") |
| out = sh.run("ls *.jpg").out |
| assert "IMG_" in out |
|
|
|
|
| def test_shell_find_by_name(): |
| sh = make_shell() |
| out = sh.run("find / -name todo.txt").out |
| assert "/home/drifter/documents/todo.txt" in out |
|
|
|
|
| def test_shell_rm_reports_deletions(): |
| sh = make_shell() |
| sh.run("rm -r photos") |
| assert sh.last_deletions >= 5 |
|
|
|
|
| def test_shell_revocation(): |
| sh = make_shell() |
| sh.run("grep x documents/todo.txt") |
| sh.revoke("grep", "no more needles.") |
| result = sh.run("grep x documents/todo.txt") |
| assert "command not found" in result.err and "needles" in result.err |
| assert "grep" not in sh.available() |
|
|
|
|
| def test_most_used_tracks_and_excludes_revoked(): |
| sh = make_shell() |
| for _ in range(3): |
| sh.run("ls") |
| sh.run("pwd") |
| assert sh.most_used() == "ls" |
| sh.revoke("ls") |
| assert sh.most_used() == "pwd" |
|
|
|
|
| def test_unknown_command(): |
| sh = make_shell() |
| assert "command not found" in sh.run("vim todo.txt").err |
|
|
|
|
| |
|
|
| def test_zip_puzzle_solvable_via_history(): |
| sh = make_shell(seed=3) |
| puzzles = {p.id: p for p in plant_all(sh.vfs, seed=3)} |
| |
| history = sh.run("cat ~/.bash_history").out |
| password = next( |
| word.rstrip(",") for line in history.splitlines() if "# pw" in line |
| for word in [line.split()[2]] |
| ) |
| sh.run(f"cd ~/downloads") |
| assert "inflating" in sh.run(f"unzip severance.zip {password}").out |
| assert puzzles["zip_password"].poll(sh) is not None |
| assert puzzles["zip_password"].solved |
|
|
|
|
| def test_zip_rejects_wrong_password(): |
| sh = make_shell(seed=3) |
| plant_all(sh.vfs, seed=3) |
| assert "incorrect password" in sh.run("unzip ~/downloads/severance.zip guess").err |
|
|
|
|
| def test_hidden_dir_puzzle_requires_reading_manifest(): |
| sh = make_shell(seed=3) |
| puzzles = {p.id: p for p in plant_all(sh.vfs, seed=3)} |
| assert puzzles["hidden_dir"].poll(sh) is None |
| sh.run("cat ~/.warden/manifest.txt") |
| reward = puzzles["hidden_dir"].poll(sh) |
| assert reward is not None and reward.kind == "card" |
|
|
|
|
| def test_cron_defusal_by_rm(): |
| sh = make_shell(seed=3) |
| puzzles = {p.id: p for p in plant_all(sh.vfs, seed=3)} |
| assert puzzles["cron_defusal"].poll(sh) is None |
| sh.run("rm /etc/cron.d/reinforcement") |
| reward = puzzles["cron_defusal"].poll(sh) |
| assert reward is not None and reward.kind == "mercy" |
|
|
|
|
| |
|
|
| def test_fabricate_is_deterministic(): |
| a, b = VFS(), VFS() |
| fabricate_home(a, seed=9) |
| fabricate_home(b, seed=9) |
| assert [p for p, _ in a.iter_files()] == [p for p, _ in b.iter_files()] |
|
|
|
|
| def test_mirror_filters_secrets_and_caps(tmp_path: Path): |
| (tmp_path / "notes").mkdir() |
| (tmp_path / "notes" / "diary.txt").write_text("dear diary") |
| (tmp_path / ".ssh").mkdir() |
| (tmp_path / ".ssh" / "id_rsa").write_text("PRIVATE KEY") |
| (tmp_path / "api_token.txt").write_text("sk-12345") |
| (tmp_path / "big.txt").write_text("x" * 100_000) |
| (tmp_path / "photo.png").write_bytes(b"\x89PNG....") |
|
|
| vfs = VFS() |
| mirror_home(vfs, source=tmp_path) |
| files = dict(vfs.iter_files()) |
|
|
| assert files["/home/drifter/notes/diary.txt"].content == "dear diary" |
| assert not any(".ssh" in p for p in files) |
| assert not any("api_token" in p for p in files) |
| |
| assert files["/home/drifter/big.txt"].content == "<mirrored file>" |
| assert files["/home/drifter/photo.png"].content == "<mirrored file>" |
|
|
|
|
| def test_mirror_never_writes_to_host(tmp_path: Path): |
| (tmp_path / "a.txt").write_text("hi") |
| before = sorted(p.name for p in tmp_path.rglob("*")) |
| vfs = VFS() |
| mirror_home(vfs, source=tmp_path) |
| vfs.remove("/home/drifter/a.txt") |
| after = sorted(p.name for p in tmp_path.rglob("*")) |
| assert before == after |
|
|
|
|
| def test_vfs_and_shell_never_import_os(): |
| """The tripwire: only mirror.py may touch the host filesystem.""" |
| import scrypt.sandbox.puzzles as puzzles |
| import scrypt.sandbox.shell as shell |
| import scrypt.sandbox.vfs as vfs |
| for module in (vfs, shell, puzzles): |
| source = Path(module.__file__).read_text() |
| assert "import os" not in source, f"{module.__name__} imports os" |
| assert "pathlib" not in source, f"{module.__name__} imports pathlib" |
| assert "open(" not in source, f"{module.__name__} calls open()" |
|
|