#!/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()