Files
box/tests/test_muse_session_bind.py

242 lines
10 KiB
Python

#!/usr/bin/env python3
"""test_muse_session_bind.py — Per-session credential isolation (P3).
Covers: session dir layout (symlinks + private auth copy), newest-wins
save-back that never touches the global auth.json, dead-only reap,
exec env, and the resume-pool session binding record.
"""
import json
import os
import stat
import sys
import time
import unittest
from pathlib import Path
from unittest import mock
REPO_ROOT = Path("/home/super/Projects/NetVM")
BIN_DIR = REPO_ROOT / "bin"
sys.path.insert(0, str(BIN_DIR))
import muse_session_bind as b
def _mkconfig(root):
"""Fake global config: profile creds + global files."""
cfg = os.path.join(root, "config")
os.makedirs(os.path.join(cfg, "accounts", "alice"))
with open(os.path.join(cfg, "accounts", "alice", "auth.json"), "wb") as f:
f.write(b"TOKEN-ALICE")
with open(os.path.join(cfg, "auth.json"), "wb") as f:
f.write(b"GLOBAL-TOKEN")
with open(os.path.join(cfg, "settings.json"), "w") as f:
f.write("{}")
with open(os.path.join(cfg, "notes.txt"), "w") as f:
f.write("hi")
return cfg
class TestBuild(unittest.TestCase):
def test_layout_links_and_private_copy(self):
import tempfile
with tempfile.TemporaryDirectory() as td:
cfg = _mkconfig(td)
parent = os.path.join(td, "run")
os.makedirs(parent)
sess = b.build_session_dir(
parent, 4242, cfg,
os.path.join(cfg, "accounts", "alice", "auth.json"),
"alice")
muse = os.path.join(sess, "muse")
# Everything but auth.json is a symlink to global.
self.assertTrue(os.path.islink(os.path.join(muse, "settings.json")))
self.assertTrue(os.path.islink(os.path.join(muse, "notes.txt")))
self.assertTrue(os.path.islink(os.path.join(muse, "accounts")))
# auth.json is a real file with the profile bytes, 0600.
auth = os.path.join(muse, "auth.json")
self.assertFalse(os.path.islink(auth))
with open(auth, "rb") as f:
self.assertEqual(f.read(), b"TOKEN-ALICE")
self.assertEqual(stat.S_IMODE(os.stat(auth).st_mode), 0o600)
bind = json.load(open(os.path.join(sess, "bind.json")))
self.assertEqual((bind["profile"], bind["pid"]),
("alice", 4242))
def test_missing_creds_refused(self):
import tempfile
with tempfile.TemporaryDirectory() as td:
cfg = _mkconfig(td)
with self.assertRaises(ValueError):
b.build_session_dir(td, 1, cfg,
os.path.join(td, "nope.json"), "alice")
def test_live_slot_refused_stale_slot_wiped(self):
import tempfile
with tempfile.TemporaryDirectory() as td:
cfg = _mkconfig(td)
auth = os.path.join(cfg, "accounts", "alice", "auth.json")
sess = b.build_session_dir(td, 99, cfg, auth, "alice")
with mock.patch.object(b, "session_liveness",
return_value="live"):
with self.assertRaises(RuntimeError):
b.build_session_dir(td, 99, cfg, auth, "alice")
with mock.patch.object(b, "session_liveness",
return_value="dead"):
sess2 = b.build_session_dir(td, 99, cfg, auth, "alice")
self.assertEqual(sess2, sess)
self.assertTrue(os.path.isfile(
os.path.join(sess2, "muse", "auth.json")))
class TestSaveBack(unittest.TestCase):
def _bound(self, td, pid=777):
cfg = _mkconfig(td)
parent = os.path.join(td, "run")
os.makedirs(parent)
sess = b.build_session_dir(
parent, pid, cfg,
os.path.join(cfg, "accounts", "alice", "auth.json"), "alice")
return cfg, sess
def test_newer_session_syncs_to_profile(self):
import tempfile
with tempfile.TemporaryDirectory() as td:
cfg, sess = self._bound(td)
prof = os.path.join(cfg, "accounts", "alice", "auth.json")
sauth = os.path.join(sess, "muse", "auth.json")
with open(sauth, "wb") as f:
f.write(b"TOKEN-REFRESHED")
now = time.time()
os.utime(prof, (now - 100, now - 100))
os.utime(sauth, (now, now))
res = b.save_session(sess, cfg)
self.assertEqual(res["status"], "synced")
with open(prof, "rb") as f:
self.assertEqual(f.read(), b"TOKEN-REFRESHED")
self.assertEqual(stat.S_IMODE(os.stat(prof).st_mode), 0o600)
# Global auth.json untouched.
with open(os.path.join(cfg, "auth.json"), "rb") as f:
self.assertEqual(f.read(), b"GLOBAL-TOKEN")
def test_stale_session_skipped(self):
import tempfile
with tempfile.TemporaryDirectory() as td:
cfg, sess = self._bound(td)
prof = os.path.join(cfg, "accounts", "alice", "auth.json")
sauth = os.path.join(sess, "muse", "auth.json")
now = time.time()
os.utime(prof, (now, now))
os.utime(sauth, (now - 100, now - 100))
res = b.save_session(sess, cfg)
self.assertEqual(res["status"], "skipped-stale")
with open(prof, "rb") as f:
self.assertEqual(f.read(), b"TOKEN-ALICE")
def test_unbound_dir_never_reaped(self):
import tempfile
with tempfile.TemporaryDirectory() as td:
foreign = os.path.join(td, "muse-session-1")
os.makedirs(foreign)
with open(os.path.join(foreign, "keep.txt"), "w") as f:
f.write("x")
self.assertEqual(b.list_bound(td), [])
res = b.reap(parent=td, config_src=os.path.join(td, "cfg"))
self.assertEqual(res["reaped"], [])
self.assertTrue(os.path.isfile(
os.path.join(foreign, "keep.txt")))
def test_reap_dead_keeps_live(self):
import tempfile
with tempfile.TemporaryDirectory() as td:
cfg = _mkconfig(td)
parent = os.path.join(td, "run")
os.makedirs(parent)
auth = os.path.join(cfg, "accounts", "alice", "auth.json")
dead = b.build_session_dir(parent, 11, cfg, auth, "alice")
live = b.build_session_dir(parent, 22, cfg, auth, "alice")
with mock.patch.object(
b, "session_liveness",
side_effect=lambda s: "live" if s == live else "dead"):
res = b.reap(parent=parent, config_src=cfg)
self.assertEqual([r["sessdir"] for r in res["reaped"]], [dead])
self.assertEqual(res["live"], [live])
self.assertFalse(os.path.exists(dead))
self.assertTrue(os.path.isdir(live))
class TestLaunch(unittest.TestCase):
def test_dry_run_plans_without_touching_disk(self):
import tempfile
with tempfile.TemporaryDirectory() as td:
cfg = _mkconfig(td)
plan = b.launch(profile="alice", config_src=cfg,
cmd=["muse-code", "--foo"], parent=td,
dry_run=True)
self.assertIn("muse-session-", plan["sessdir"])
self.assertEqual(plan["xdg_config_home"], plan["sessdir"])
self.assertTrue(plan["auth_src"].endswith(
"accounts/alice/auth.json"))
self.assertFalse(os.path.exists(plan["sessdir"]))
def test_launch_execs_with_isolated_env(self):
import tempfile
with tempfile.TemporaryDirectory() as td:
cfg = _mkconfig(td)
seen = {}
def fake_exec(path, argv, env):
seen.update(path=path, argv=argv, env=env)
with mock.patch.object(b.os, "getpid", return_value=555):
b.launch(profile="alice", config_src=cfg,
cmd=["muse-code", "chat"], parent=td,
_exec=fake_exec)
sess = os.path.join(td, "muse-session-555")
self.assertEqual(seen["path"], "muse-code")
self.assertEqual(seen["env"]["XDG_CONFIG_HOME"], sess)
self.assertEqual(seen["env"]["MUSE_SESSION_BIND_DIR"], sess)
self.assertTrue(os.path.isfile(
os.path.join(sess, "muse", "auth.json")))
def test_launch_records_session_profile(self):
import tempfile
with tempfile.TemporaryDirectory() as td:
cfg = _mkconfig(td)
with mock.patch.object(b.os, "getpid", return_value=556):
b.launch(profile="alice", config_src=cfg,
session_id="sess-1", cmd=["muse-code"],
parent=td, _exec=lambda *a: None)
data = json.load(open(os.path.join(cfg, "session_profiles.json")))
self.assertEqual(data, {"sess-1": "alice"})
def test_launch_without_profile_or_auth_refused(self):
with self.assertRaises(ValueError):
b.launch(cmd=["muse-code"], dry_run=True)
class TestLiveness(unittest.TestCase):
def test_recycled_pid_is_dead(self):
import tempfile
with tempfile.TemporaryDirectory() as td:
sess = os.path.join(td, "muse-session-9")
os.makedirs(sess)
with open(os.path.join(sess, "bind.json"), "w") as f:
json.dump({"profile": "alice", "pid": 9}, f)
with mock.patch.object(b, "_pid_alive", return_value=True), \
mock.patch.object(b, "_pid_is_muse", return_value=False):
self.assertEqual(b.session_liveness(sess), "dead")
def test_pid_is_muse_matches_binary_names(self):
import io
with mock.patch("builtins.open",
mock.mock_open(read_data=b"muse-bin-1.4\x00--x\x00")):
self.assertTrue(b._pid_is_muse(123))
with mock.patch("builtins.open",
mock.mock_open(read_data=b"python3\x00foo\x00")):
self.assertFalse(b._pid_is_muse(123))
if __name__ == "__main__":
unittest.main()