feat(muse): harden choice watcher concurrency, add rules dictionary, resume pool, and session bind
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user