284 lines
9.0 KiB
Python
284 lines
9.0 KiB
Python
"""Tests for the OS environment fork (copy-on-write) mode."""
|
|
|
|
import asyncio
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
|
|
from omnigent.inner.datamodel import OSEnvSandboxSpec, OSEnvSpec
|
|
from omnigent.inner.os_env import (
|
|
_copy_tree,
|
|
create_os_environment,
|
|
)
|
|
|
|
|
|
def _run(coro):
|
|
loop = asyncio.new_event_loop()
|
|
try:
|
|
return loop.run_until_complete(coro)
|
|
finally:
|
|
loop.close()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Unit tests for copy tree helper
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestCopyTree(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmpdir = tempfile.mkdtemp()
|
|
self.src = Path(self.tmpdir) / "src"
|
|
self.dst = Path(self.tmpdir) / "dst"
|
|
|
|
def tearDown(self):
|
|
import shutil
|
|
|
|
shutil.rmtree(self.tmpdir, ignore_errors=True)
|
|
|
|
def test_files_are_copied(self):
|
|
self.src.mkdir()
|
|
(self.src / "a.txt").write_text("aaa")
|
|
(self.src / "b.txt").write_text("bbb")
|
|
|
|
_copy_tree(self.src, self.dst)
|
|
self.assertEqual((self.dst / "a.txt").read_text(), "aaa")
|
|
self.assertEqual((self.dst / "b.txt").read_text(), "bbb")
|
|
|
|
# Different inodes (independent copies)
|
|
self.assertNotEqual(
|
|
(self.src / "a.txt").stat().st_ino,
|
|
(self.dst / "a.txt").stat().st_ino,
|
|
)
|
|
|
|
def test_subdirectories_are_recreated(self):
|
|
(self.src / "sub" / "deep").mkdir(parents=True)
|
|
(self.src / "sub" / "deep" / "file.txt").write_text("deep content")
|
|
|
|
_copy_tree(self.src, self.dst)
|
|
self.assertEqual((self.dst / "sub" / "deep" / "file.txt").read_text(), "deep content")
|
|
|
|
def test_symlinks_are_copied_as_symlinks(self):
|
|
self.src.mkdir()
|
|
(self.src / "target.txt").write_text("target")
|
|
(self.src / "link.txt").symlink_to("target.txt")
|
|
|
|
_copy_tree(self.src, self.dst)
|
|
self.assertTrue((self.dst / "link.txt").is_symlink())
|
|
self.assertEqual(os.readlink(str(self.dst / "link.txt")), "target.txt")
|
|
|
|
def test_empty_directory(self):
|
|
self.src.mkdir()
|
|
_copy_tree(self.src, self.dst)
|
|
self.assertTrue(self.dst.is_dir())
|
|
|
|
def test_writes_to_copy_dont_affect_original(self):
|
|
self.src.mkdir()
|
|
(self.src / "file.txt").write_text("original")
|
|
_copy_tree(self.src, self.dst)
|
|
|
|
(self.dst / "file.txt").write_text("modified")
|
|
self.assertEqual((self.src / "file.txt").read_text(), "original")
|
|
self.assertEqual((self.dst / "file.txt").read_text(), "modified")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Integration tests: forked CallerProcessOSEnvironment
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestForkedOSEnvironment(unittest.TestCase):
|
|
"""Test the fork mode end-to-end through the helper process."""
|
|
|
|
def setUp(self):
|
|
self.tmpdir = tempfile.mkdtemp()
|
|
self.src = Path(self.tmpdir) / "project"
|
|
self.src.mkdir()
|
|
(self.src / "readme.txt").write_text("original readme\n")
|
|
(self.src / "data.txt").write_text("original data\n")
|
|
(self.src / "sub").mkdir()
|
|
(self.src / "sub" / "nested.txt").write_text("nested content\n")
|
|
|
|
def tearDown(self):
|
|
import shutil
|
|
|
|
shutil.rmtree(self.tmpdir, ignore_errors=True)
|
|
|
|
def _make_env(self):
|
|
spec = OSEnvSpec(
|
|
type="caller_process",
|
|
cwd=str(self.src),
|
|
fork=True,
|
|
sandbox=OSEnvSandboxSpec(type="none"),
|
|
)
|
|
return create_os_environment(spec)
|
|
|
|
def test_read_sees_original_content(self):
|
|
env = self._make_env()
|
|
try:
|
|
result = _run(env.read("readme.txt"))
|
|
self.assertIn("original readme", result["content"])
|
|
finally:
|
|
env.close()
|
|
|
|
def test_write_does_not_modify_original(self):
|
|
env = self._make_env()
|
|
try:
|
|
_run(env.write("readme.txt", "modified readme\n"))
|
|
|
|
# Forked env sees the modification
|
|
result = _run(env.read("readme.txt"))
|
|
self.assertIn("modified readme", result["content"])
|
|
|
|
# Original is untouched
|
|
self.assertEqual((self.src / "readme.txt").read_text(), "original readme\n")
|
|
finally:
|
|
env.close()
|
|
|
|
def test_edit_does_not_modify_original(self):
|
|
env = self._make_env()
|
|
try:
|
|
_run(env.edit("data.txt", old_text="original", new_text="edited"))
|
|
|
|
# Forked env sees the edit
|
|
result = _run(env.read("data.txt"))
|
|
self.assertIn("edited data", result["content"])
|
|
|
|
# Original is untouched
|
|
self.assertEqual((self.src / "data.txt").read_text(), "original data\n")
|
|
finally:
|
|
env.close()
|
|
|
|
def test_write_new_file_does_not_appear_in_original(self):
|
|
env = self._make_env()
|
|
try:
|
|
_run(env.write("new_file.txt", "brand new\n"))
|
|
result = _run(env.read("new_file.txt"))
|
|
self.assertIn("brand new", result["content"])
|
|
|
|
# Original directory does not have the new file
|
|
self.assertFalse((self.src / "new_file.txt").exists())
|
|
finally:
|
|
env.close()
|
|
|
|
def test_shell_does_not_modify_original(self):
|
|
env = self._make_env()
|
|
try:
|
|
# Shell write via redirect
|
|
_run(env.shell("echo 'shell wrote this' > readme.txt"))
|
|
|
|
# Forked env sees it
|
|
result = _run(env.read("readme.txt"))
|
|
self.assertIn("shell wrote this", result["content"])
|
|
|
|
# Original is untouched (hardlinks were broken before shell ran)
|
|
self.assertEqual((self.src / "readme.txt").read_text(), "original readme\n")
|
|
finally:
|
|
env.close()
|
|
|
|
def test_nested_file_isolation(self):
|
|
env = self._make_env()
|
|
try:
|
|
_run(env.write("sub/nested.txt", "modified nested\n"))
|
|
|
|
result = _run(env.read("sub/nested.txt"))
|
|
self.assertIn("modified nested", result["content"])
|
|
|
|
self.assertEqual(
|
|
(self.src / "sub" / "nested.txt").read_text(),
|
|
"nested content\n",
|
|
)
|
|
finally:
|
|
env.close()
|
|
|
|
def test_cleanup_removes_fork_dir(self):
|
|
env = self._make_env()
|
|
fork_dir = env._fork_dir
|
|
self.assertIsNotNone(fork_dir)
|
|
self.assertTrue(fork_dir.exists())
|
|
|
|
env.close()
|
|
self.assertFalse(fork_dir.exists())
|
|
|
|
def test_multiple_writes_to_same_file(self):
|
|
env = self._make_env()
|
|
try:
|
|
_run(env.write("readme.txt", "first write\n"))
|
|
_run(env.write("readme.txt", "second write\n"))
|
|
|
|
result = _run(env.read("readme.txt"))
|
|
self.assertIn("second write", result["content"])
|
|
|
|
self.assertEqual((self.src / "readme.txt").read_text(), "original readme\n")
|
|
finally:
|
|
env.close()
|
|
|
|
def test_shell_absolute_path_reads_fork_not_original(self):
|
|
"""Shell commands using relative paths operate on the fork tree."""
|
|
env = self._make_env()
|
|
try:
|
|
# Write a marker into the fork via the helper
|
|
_run(env.write("marker.txt", "fork marker\n"))
|
|
# Shell reads it
|
|
result = _run(env.shell("cat marker.txt"))
|
|
self.assertIn("fork marker", result["stdout"])
|
|
# Original should not have marker.txt
|
|
self.assertFalse((self.src / "marker.txt").exists())
|
|
finally:
|
|
env.close()
|
|
|
|
def test_edit_after_write_in_fork(self):
|
|
env = self._make_env()
|
|
try:
|
|
_run(env.write("data.txt", "line one\nline two\n"))
|
|
_run(env.edit("data.txt", old_text="line two", new_text="line TWO"))
|
|
|
|
result = _run(env.read("data.txt"))
|
|
self.assertIn("line TWO", result["content"])
|
|
|
|
self.assertEqual((self.src / "data.txt").read_text(), "original data\n")
|
|
finally:
|
|
env.close()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# YAML loading
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestForkYAMLLoading(unittest.TestCase):
|
|
def test_loader_parses_fork_field(self):
|
|
from omnigent.inner.loader import load_agent_def
|
|
|
|
agent = load_agent_def(
|
|
{
|
|
"name": "test",
|
|
"os_env": {
|
|
"type": "caller_process",
|
|
"fork": True,
|
|
},
|
|
}
|
|
)
|
|
self.assertTrue(agent.os_env.fork)
|
|
|
|
def test_loader_default_fork_false(self):
|
|
from omnigent.inner.loader import load_agent_def
|
|
|
|
agent = load_agent_def(
|
|
{
|
|
"name": "test",
|
|
"os_env": {
|
|
"type": "caller_process",
|
|
},
|
|
}
|
|
)
|
|
self.assertFalse(agent.os_env.fork)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|