import json import os import shutil import subprocess import sys import tempfile import time import unittest from unittest import mock import shellmcp HERE = os.path.dirname(os.path.abspath(__file__)) class Run(unittest.TestCase): def test_exit_code_and_combined_output(self): self.assertEqual(shellmcp.run("echo hi; echo oops >&2; exit 3", HERE), "exit code 3\nhi\noops\n") def test_long_output_keeps_head_and_tail(self): out = shellmcp.run("printf 'a%.0s' $(seq 100); printf 'b%.0s' $(seq 100)", HERE, limit=20) self.assertEqual(out, "exit code 0\naaaaaaaaaa\n[... 180 characters cut ...]\nbbbbbbbbbb") def test_timeout_kills_the_whole_group(self): start = time.monotonic() out = shellmcp.run("sleep 30 & sleep 30", HERE, timeout=1) self.assertLess(time.monotonic() - start, 5) self.assertEqual(out, "timed out after 1 s, killed\n") def test_no_stdin_and_no_pager(self): self.assertEqual(shellmcp.run("cat; echo $PAGER", HERE), "exit code 0\ncat\n") @unittest.skipUnless(shutil.which("bwrap"), "needs bwrap") class Sandbox(unittest.TestCase): def test_readonly_cannot_write_and_hides_paths(self): with tempfile.TemporaryDirectory(dir=HERE) as d: # not /tmp: the sandbox replaces /tmp os.mkdir(os.path.join(d, "secret")) open(os.path.join(d, "secret", "key"), "w").close() prefix = shellmcp.sandbox(d, [os.path.join(d, "secret")]) out = shellmcp.run("touch new; ls secret; echo > /tmp/x && echo tmp-ok", d, prefix=prefix) self.assertIn("Read-only file system", out) self.assertNotIn("key", out) self.assertIn("tmp-ok", out) self.assertFalse(os.path.exists(os.path.join(d, "new")))