from pathlib import Path from hqt import sandbox from hqt.sandbox import Bind, SandboxPolicy def test_bind_defaults_readonly(): b = Bind(Path("/home/u/.claude")) assert b.src == Path("/home/u/.claude") assert b.writable is False def test_sandbox_policy_fields(): p = SandboxPolicy(fs="rw", net=True) assert p.fs == "rw" assert p.net is True def test_is_available_true_on_linux_with_bwrap(monkeypatch): monkeypatch.setattr(sandbox.sys, "platform", "linux") monkeypatch.setattr(sandbox.shutil, "which", lambda name: "/usr/bin/bwrap") assert sandbox.is_available() is True def test_is_available_false_without_bwrap(monkeypatch): monkeypatch.setattr(sandbox.sys, "platform", "linux") monkeypatch.setattr(sandbox.shutil, "which", lambda name: None) assert sandbox.is_available() is False def test_is_available_false_off_linux(monkeypatch): monkeypatch.setattr(sandbox.sys, "platform", "darwin") monkeypatch.setattr(sandbox.shutil, "which", lambda name: "/usr/bin/bwrap") assert sandbox.is_available() is False def _split(args, flag): """Return list of (src, dst) pairs that follow each occurrence of flag.""" out = [] for i, a in enumerate(args): if a == flag: out.append((args[i + 1], args[i + 2])) return out def test_wrap_command_after_double_dash(tmp_path): cmd = ["claude", "--session-id", "x"] args = sandbox.wrap(cmd, tmp_path, SandboxPolicy(fs="rw", net=True), []) assert args[0] == "bwrap" assert "--" in args assert args[args.index("--") + 1 :] == cmd def test_wrap_base_flags(tmp_path): args = sandbox.wrap(["sh"], tmp_path, SandboxPolicy(fs="rw", net=True), []) assert "--die-with-parent" in args assert "--unshare-all" in args assert "--proc" in args and "--dev" in args def test_wrap_net_on_shares_net(tmp_path): args = sandbox.wrap(["sh"], tmp_path, SandboxPolicy(fs="rw", net=True), []) assert "--share-net" in args def test_wrap_net_off_does_not_share(tmp_path): args = sandbox.wrap(["sh"], tmp_path, SandboxPolicy(fs="rw", net=False), []) assert "--share-net" not in args def test_wrap_cwd_rw_is_bind(tmp_path): args = sandbox.wrap(["sh"], tmp_path, SandboxPolicy(fs="rw", net=True), []) assert (str(tmp_path), str(tmp_path)) in _split(args, "--bind") def test_wrap_cwd_ro_is_ro_bind(tmp_path): args = sandbox.wrap(["sh"], tmp_path, SandboxPolicy(fs="ro", net=True), []) assert (str(tmp_path), str(tmp_path)) in _split(args, "--ro-bind") def test_wrap_writable_bind_spliced(tmp_path): cred = tmp_path / "cred" cred.mkdir() args = sandbox.wrap( ["sh"], tmp_path, SandboxPolicy(fs="rw", net=True), [Bind(cred, writable=True)] ) assert (str(cred), str(cred)) in _split(args, "--bind") def test_wrap_readonly_bind_spliced(tmp_path): cred = tmp_path / "cred" cred.mkdir() args = sandbox.wrap( ["sh"], tmp_path, SandboxPolicy(fs="rw", net=True), [Bind(cred)] ) assert (str(cred), str(cred)) in _split(args, "--ro-bind") def test_wrap_skips_missing_binds(tmp_path): missing = tmp_path / "nope" args = sandbox.wrap( ["sh"], tmp_path, SandboxPolicy(fs="rw", net=True), [Bind(missing)] ) assert str(missing) not in args def test_wrap_binds_gitconfig_readonly_when_present(tmp_path, monkeypatch): """~/.gitconfig is bound read-only so `git commit` works inside the jail.""" home = tmp_path / "home" home.mkdir() gitconfig = home / ".gitconfig" gitconfig.write_text("[user]\n\tname = Test\n") monkeypatch.setattr(sandbox.Path, "home", classmethod(lambda cls: home)) args = sandbox.wrap(["sh"], tmp_path, SandboxPolicy(fs="rw", net=True), []) assert (str(gitconfig), str(gitconfig)) in _split(args, "--ro-bind") def test_wrap_skips_gitconfig_when_absent(tmp_path, monkeypatch): home = tmp_path / "home" home.mkdir() monkeypatch.setattr(sandbox.Path, "home", classmethod(lambda cls: home)) args = sandbox.wrap(["sh"], tmp_path, SandboxPolicy(fs="rw", net=True), []) assert str(home / ".gitconfig") not in args