"""Tests for flow.core.runtime.""" import json import os from pathlib import Path import pytest from flow.core.containers import ContainerRuntime from flow.core.errors import FlowError from flow.core.runtime import CommandRunner, FileSystem, GitClient, SystemRuntime from flow.core.tmux import TmuxClient class TestFileSystem: def test_ensure_dir_creates_nested(self, tmp_path): fs = FileSystem() target = tmp_path / "a" / "b" / "c" fs.ensure_dir(target) assert target.is_dir() def test_write_and_read_text(self, tmp_path): fs = FileSystem() path = tmp_path / "test.txt" fs.write_text(path, "hello") assert fs.read_text(path) == "hello" def test_read_text_default(self, tmp_path): fs = FileSystem() path = tmp_path / "missing.txt" assert fs.read_text(path, default="fallback") == "fallback" def test_write_and_read_json(self, tmp_path): fs = FileSystem() path = tmp_path / "data.json" fs.write_json(path, {"key": "value"}) assert fs.read_json(path) == {"key": "value"} def test_create_symlink(self, tmp_path): fs = FileSystem() source = tmp_path / "source" source.write_text("content") target = tmp_path / "link" fs.create_symlink(source, target) assert target.is_symlink() assert target.resolve() == source.resolve() def test_same_symlink_true(self, tmp_path): fs = FileSystem() source = tmp_path / "source" source.write_text("content") target = tmp_path / "link" target.symlink_to(source) assert fs.same_symlink(target, source) is True def test_same_symlink_false(self, tmp_path): fs = FileSystem() source = tmp_path / "source" source.write_text("content") other = tmp_path / "other" other.write_text("other") target = tmp_path / "link" target.symlink_to(other) assert fs.same_symlink(target, source) is False def test_remove_file(self, tmp_path): fs = FileSystem() path = tmp_path / "file" path.write_text("x") fs.remove_file(path) assert not path.exists() def test_remove_file_missing_ok(self, tmp_path): fs = FileSystem() fs.remove_file(tmp_path / "missing", missing_ok=True) # no error def test_copy_file(self, tmp_path): fs = FileSystem() src = tmp_path / "src" src.write_text("data") dst = tmp_path / "sub" / "dst" fs.copy_file(src, dst) assert dst.read_text() == "data" def test_write_json_atomic_leaves_no_tmp_residue(self, tmp_path): fs = FileSystem() path = tmp_path / "state" / "data.json" fs.write_json(path, {"a": 1}) assert path.read_text() assert json.loads(path.read_text()) == {"a": 1} residue = list(path.parent.glob("*.tmp")) assert residue == [] def test_write_json_overwrites_stale_tmp(self, tmp_path): fs = FileSystem() path = tmp_path / "data.json" stale = path.with_suffix(path.suffix + ".tmp") stale.write_text("garbage") fs.write_json(path, {"k": "v"}) assert json.loads(path.read_text()) == {"k": "v"} assert not stale.exists() def test_remove_tree_raises_when_missing(self, tmp_path): fs = FileSystem() with pytest.raises(FileNotFoundError): fs.remove_tree(tmp_path / "absent") def test_remove_tree_missing_ok(self, tmp_path): fs = FileSystem() fs.remove_tree(tmp_path / "absent", missing_ok=True) # no error def test_remove_tree_raises_on_permission_error(self, tmp_path): fs = FileSystem() # Create a parent dir we can't traverse, holding a child directory. # If we can't actually drop permissions (e.g. running as root), skip. if os.geteuid() == 0: pytest.skip("Running as root: cannot simulate permission error") parent = tmp_path / "locked" parent.mkdir() (parent / "child").mkdir() try: parent.chmod(0o500) # r-x: can list but not modify with pytest.raises(OSError): fs.remove_tree(parent / "child") finally: parent.chmod(0o700) def test_create_symlink_refuses_real_file(self, tmp_path): fs = FileSystem() source = tmp_path / "source" source.write_text("src") target = tmp_path / "target" target.write_text("user content") with pytest.raises(FlowError, match="Refusing to overwrite"): fs.create_symlink(source, target) # Target untouched. assert target.read_text() == "user content" def test_create_symlink_refuses_foreign_symlink(self, tmp_path): fs = FileSystem() source = tmp_path / "source" source.write_text("src") elsewhere = tmp_path / "elsewhere" elsewhere.write_text("other") target = tmp_path / "target" target.symlink_to(elsewhere) with pytest.raises(FlowError, match="Refusing to overwrite"): fs.create_symlink(source, target) # Foreign symlink preserved. assert target.readlink() == elsewhere def test_create_symlink_when_target_absent(self, tmp_path): fs = FileSystem() source = tmp_path / "source" source.write_text("src") target = tmp_path / "sub" / "target" fs.create_symlink(source, target) assert target.is_symlink() assert target.readlink() == source def test_create_symlink_idempotent_overwrite(self, tmp_path): fs = FileSystem() source = tmp_path / "source" source.write_text("src") target = tmp_path / "target" target.symlink_to(source) fs.create_symlink(source, target) # must not raise assert target.is_symlink() assert target.readlink() == source def test_remove_symlink_with_matching_expected(self, tmp_path): fs = FileSystem() source = tmp_path / "source" source.write_text("src") target = tmp_path / "target" target.symlink_to(source) fs.remove_symlink(target, expected_source=source) assert not target.exists() assert not target.is_symlink() def test_remove_symlink_refuses_regular_file(self, tmp_path): fs = FileSystem() target = tmp_path / "target" target.write_text("real file") with pytest.raises(FlowError, match="Refusing to remove non-symlink"): fs.remove_symlink(target) assert target.read_text() == "real file" def test_remove_symlink_refuses_mismatched_expected(self, tmp_path): fs = FileSystem() source = tmp_path / "source" source.write_text("src") elsewhere = tmp_path / "elsewhere" elsewhere.write_text("other") target = tmp_path / "target" target.symlink_to(elsewhere) with pytest.raises(FlowError, match="Refusing to remove symlink"): fs.remove_symlink(target, expected_source=source) # Untouched. assert target.is_symlink() assert target.readlink() == elsewhere def test_remove_symlink_absent_is_noop(self, tmp_path): fs = FileSystem() fs.remove_symlink(tmp_path / "missing") # no error class TestCommandRunner: def test_run_echo(self): runner = CommandRunner() result = runner.run(["echo", "hello"], capture_output=True) assert result.stdout.strip() == "hello" def test_require_binary_finds_echo(self): runner = CommandRunner() path = runner.require_binary("echo") assert path is not None class TestSystemRuntime: def test_creates_git_client(self): rt = SystemRuntime() assert isinstance(rt.git, GitClient) assert rt.git.runner is rt.runner def test_creates_tmux_client(self): rt = SystemRuntime() assert isinstance(rt.tmux, TmuxClient) assert rt.tmux.runner is rt.runner def test_creates_container_runtime(self): rt = SystemRuntime() assert isinstance(rt.containers, ContainerRuntime) assert rt.containers.runner is rt.runner