feat(muse): harden choice watcher concurrency, add rules dictionary, resume pool, and session bind

This commit is contained in:
operator
2026-10-07 01:50:18 +00:00
parent 90f4ef661a
commit 094bd7d691
10 changed files with 2166 additions and 40 deletions
+196
View File
@@ -505,6 +505,83 @@ class TestPidfileClaim(unittest.TestCase):
self.assertTrue(w._claim_pidfile(path))
class TestAnsweredPersistence(unittest.TestCase):
"""Answered sigs survive restarts: a daemon that restarts while an
answered prompt is still visible must not answer it again (live
double-answer -> stray "1" in the input box)."""
def test_restart_suppresses_answered_sig(self):
import tempfile
with tempfile.TemporaryDirectory() as td:
path = td + "/x.answered.json"
s1 = w.WatcherState(persist_path=path)
now = time.time()
s1.record_answer("sig-abc", now)
s2 = w.WatcherState(persist_path=path)
self.assertEqual(s2.observe({"sig": "sig-abc"}, now + 5),
"none")
def test_expired_sig_not_loaded(self):
import tempfile
with tempfile.TemporaryDirectory() as td:
path = td + "/x.answered.json"
s1 = w.WatcherState(persist_path=path)
s1.record_answer("sig-old",
time.time() - w.ANSWERED_TTL_SECONDS - 10)
s2 = w.WatcherState(persist_path=path)
self.assertNotIn("sig-old", s2.answered_sigs)
def test_corrupt_store_loads_empty(self):
import tempfile
with tempfile.TemporaryDirectory() as td:
path = td + "/x.answered.json"
with open(path, "w") as f:
f.write("not json{{{")
s = w.WatcherState(persist_path=path)
self.assertEqual(s.answered_sigs, {})
def test_memory_only_without_path(self):
s = w.WatcherState()
s.record_answer("sig-x", time.time())
self.assertIn("sig-x", s.answered_sigs)
class TestPidfileFlock(unittest.TestCase):
"""Single daemon per pane, kernel-enforced: concurrent starters
must not pile up (double answers, '11' in the input box)."""
def _child_claim(self, path):
import subprocess
code = ("import sys; sys.path.insert(0, %r); "
"import muse_choice_watcher as w; "
"sys.exit(0 if w._claim_pidfile(%r) else 3)") % (
str(BIN_DIR), path)
return subprocess.run([sys.executable, "-c", code],
capture_output=True, timeout=30)
def test_second_process_refused_while_held(self):
import tempfile
with tempfile.TemporaryDirectory() as td:
path = td + "/x.pid"
self.assertTrue(w._claim_pidfile(path))
try:
r = self._child_claim(path)
self.assertEqual(r.returncode, 3)
finally:
w._release_pidfile(path)
def test_claim_succeeds_after_release(self):
import os
import tempfile
with tempfile.TemporaryDirectory() as td:
path = td + "/x.pid"
self.assertTrue(w._claim_pidfile(path))
w._release_pidfile(path)
self.assertFalse(os.path.exists(path))
r = self._child_claim(path)
self.assertEqual(r.returncode, 0)
class TestReconcileFailed(unittest.TestCase):
def test_failed_starts_recorded(self):
with mock.patch.object(w, "get_desired",
@@ -1451,5 +1528,124 @@ class TestBoxResolve(unittest.TestCase):
self.assertIsNone(data["held"])
class TestPeerAnsweredReload(unittest.TestCase):
"""A peer watcher answering first must suppress this watcher.
Concurrent duplicates share only the on-disk answered store: a
second watcher that loaded before the peer's answer must re-check
the store before typing, or both type '1' ('11' in the input box).
"""
def test_disk_answer_suppresses_send(self):
import json
import tempfile
import time
with tempfile.TemporaryDirectory() as td:
path = td + "/x.answered.json"
state = w.WatcherState(persist_path=path)
log = mock.Mock()
sig = w.find_choice_prompt(PROMPT_YN)["sig"]
with mock.patch.object(w, "pane_exists", return_value=True), \
mock.patch.object(w, "capture_pane",
side_effect=[PROMPT_YN] * 4), \
mock.patch.object(w, "send_answer",
return_value=True) as send, \
mock.patch.object(w, "audit"):
out1 = w._poll_once("/tmp/s", "%1", state, log,
dry_run=False)
self.assertEqual(out1, "seen")
# Peer answers the same prompt and persists first.
with open(path, "w") as f:
json.dump({sig: time.time()}, f)
out2 = w._poll_once("/tmp/s", "%1", state, log,
dry_run=False)
send.assert_not_called()
self.assertIn(out2, ("duplicate-suppressed", "none"))
self.assertIn(sig, state.answered_sigs)
class TestCaptureJoinWrapped(unittest.TestCase):
def test_capture_joins_wrapped_lines(self):
with mock.patch.object(w, "_tmux") as t:
t.return_value = mock.Mock(returncode=0, stdout="ok")
w.capture_pane("/tmp/s", "%1")
args = t.call_args[0]
self.assertIn("-J", args)
class TestLogPosture(unittest.TestCase):
"""Watcher start records the pane's permission posture.
The watcher answers with per-choice logging in every mode; bypass
postures (yolo / approval disabled) additionally audit, since the
session then acts outside the choice trail.
"""
def test_bypass_audits(self):
log = mock.Mock()
with mock.patch.object(w, "pane_muse_argv",
return_value=["muse", "--yolo"]), \
mock.patch.object(w, "audit") as audit:
w._log_posture("/tmp/s", "%1", log)
log.log.assert_called_once()
self.assertEqual(log.log.call_args[0][1], "pane posture")
self.assertEqual(log.log.call_args[1]["mode"], "yolo")
audit.assert_called_once()
self.assertEqual(audit.call_args[0][0], "muse-choice-posture")
self.assertTrue(audit.call_args[1]["extra"]["bypass"])
def test_default_logs_without_audit(self):
log = mock.Mock()
with mock.patch.object(w, "pane_muse_argv",
return_value=["muse"]), \
mock.patch.object(w, "audit") as audit:
w._log_posture("/tmp/s", "%1", log)
log.log.assert_called_once()
audit.assert_not_called()
def test_nondefault_profile_audits(self):
log = mock.Mock()
with mock.patch.object(w, "pane_muse_argv",
return_value=["muse", "--permission-profile",
":unrestricted"]), \
mock.patch.object(w, "audit") as audit:
w._log_posture("/tmp/s", "%1", log)
audit.assert_called_once()
extra = audit.call_args[1]["extra"]
self.assertEqual(extra["mode"], ":unrestricted")
self.assertFalse(extra["bypass"])
def test_argv_failure_never_raises(self):
log = mock.Mock()
with mock.patch.object(w, "pane_muse_argv",
side_effect=RuntimeError("tmux")), \
mock.patch.object(w, "audit") as audit:
w._log_posture("/tmp/s", "%1", log) # must not raise
log.log.assert_not_called()
audit.assert_not_called()
class TestReconcileOrphan(unittest.TestCase):
"""Reconcile must not start a second watcher when an orphan with a
lost pidfile is already alive for the pane."""
def test_orphan_counts_as_already(self):
orphan = [{"pid": 7777, "socket": "/tmp/sock", "pane": "%37"}]
with mock.patch.object(w, "get_desired",
return_value={"enabled": True,
"dry_run": False}), \
mock.patch.object(w, "muse_panes", return_value=["%37"]), \
mock.patch.object(w, "is_running", return_value=None), \
mock.patch.object(w, "_watch_procs", return_value=orphan), \
mock.patch.object(w, "_start_detached") as start, \
mock.patch.object(w, "status_all", return_value=[]), \
mock.patch.object(w, "audit"), \
mock.patch.object(w.os.path, "exists", return_value=True):
res = w.reconcile(sockets=["/tmp/sock"])
start.assert_not_called()
self.assertEqual(res["already"], ["/tmp/sock:%37"])
self.assertEqual(res["failed"], [])
if __name__ == "__main__":
unittest.main()
+286
View File
@@ -0,0 +1,286 @@
#!/usr/bin/env python3
"""test_muse_resume_pool.py — focused tests for the per-repo, profile-aware
Muse Code resume pool and guard. Fixture-only: never touches the real
~/.config/muse or ~/.local/share/muse."""
import json
import os
import sqlite3
import sys
import tempfile
import unittest
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(REPO_ROOT, "bin"))
import muse_resume_pool as mrp
WS_NETVM = "/home/super/Projects/NetVM"
WS_HOME = "/home/super"
def make_row(session_id, workspace, name=None, created_us=1791000000000000,
updated_us=1791300000000000, status="valid", title="t"):
return {
"session_id": session_id,
"session_name": name,
"workspace_root": workspace,
"workspace_key": workspace,
"provider_id": "meta",
"model_id": "m",
"git_branch": None,
"title": title,
"first_user_prompt": title,
"created_at_us": created_us,
"updated_at_us": updated_us,
"prompt_count": 3,
"status": status,
}
def make_auth(active="muse", mapping=None, history=None, readable=True):
return {
"readable": readable,
"active": active,
"session_profiles": dict(mapping or {}),
"switch_history": list(history or []),
}
class TestProfileResolution(unittest.TestCase):
def test_cached_mapping_wins_over_history(self):
auth = make_auth(
mapping={"s1": "def"},
history=[{"profile": "muse", "epoch": 1790000000.0}],
)
profile, source = mrp.resolve_profile("s1", 1791000000000000, auth)
self.assertEqual((profile, source), ("def", "cached"))
def test_history_picks_latest_switch_before_start(self):
auth = make_auth(history=[
{"profile": "muse", "epoch": 1790000000.0},
{"profile": "def", "epoch": 1791100000.0},
{"profile": "opm", "epoch": 1791400000.0},
])
profile, source = mrp.resolve_profile("sx", 1791200000000000, auth)
self.assertEqual((profile, source), ("def", "history"))
def test_history_predating_all_switches_uses_earliest(self):
auth = make_auth(history=[
{"profile": "def", "epoch": 1791100000.0},
{"profile": "opm", "epoch": 1791400000.0},
])
profile, source = mrp.resolve_profile("sx", 1790000000000000, auth)
self.assertEqual((profile, source), ("def", "history"))
def test_no_history_falls_back_to_active(self):
auth = make_auth(active="pip", history=[])
profile, source = mrp.resolve_profile("sx", 1791200000000000, auth)
self.assertEqual((profile, source), ("pip", "fallback"))
def test_unreadable_auth_dir_is_unknown(self):
auth = make_auth(readable=False)
profile, source = mrp.resolve_profile("s1", 1791200000000000, auth)
self.assertEqual((profile, source), (None, "unknown"))
class TestWorkspacePool(unittest.TestCase):
def test_exact_workspace_match_only(self):
rows = [
make_row("a" * 32, WS_NETVM, name="n1"),
make_row("b" * 32, WS_HOME, name="n2"),
make_row("c" * 32, WS_NETVM, name="n3"),
]
pool = mrp.pool_for_workspace(rows, WS_NETVM)
self.assertEqual([r["session_id"] for r in pool], ["a" * 32, "c" * 32])
def test_subdirectory_does_not_match(self):
rows = [make_row("a" * 32, WS_NETVM + "/subdir")]
self.assertEqual(mrp.pool_for_workspace(rows, WS_NETVM), [])
def test_split_hides_only_proven_mismatches(self):
rows = [
make_row("a" * 32, WS_NETVM),
make_row("b" * 32, WS_NETVM),
make_row("c" * 32, WS_NETVM),
]
rows[0]["auth_profile"] = "muse"
rows[1]["auth_profile"] = "def"
rows[2]["auth_profile"] = None # unknown stays visible, flagged
resumable, blocked = mrp.split_resumable(rows, "muse")
self.assertEqual([r["session_id"] for r in resumable], ["a" * 32, "c" * 32])
self.assertEqual([r["session_id"] for r in blocked], ["b" * 32])
class TestRefResolution(unittest.TestCase):
def setUp(self):
self.rows = [
make_row("01a113f1" + "0" * 24, WS_NETVM, name="alder-halo"),
make_row("01a113d8" + "1" * 24, WS_NETVM, name="blue-merak"),
make_row("01a113f1" + "2" * 24, WS_NETVM, name="third-wheel"),
]
def test_full_uuid(self):
matches, kind = mrp.resolve_ref(self.rows, "01a113f1" + "0" * 24)
self.assertEqual((len(matches), kind), (1, "uuid"))
def test_name(self):
matches, kind = mrp.resolve_ref(self.rows, "blue-merak")
self.assertEqual((len(matches), kind), (1, "name"))
def test_prefix(self):
matches, kind = mrp.resolve_ref(self.rows, "01a113f100")
self.assertEqual((len(matches), kind), (1, "prefix"))
def test_short_prefix_rejected(self):
matches, kind = mrp.resolve_ref(self.rows, "01a1")
self.assertEqual((matches, kind), ([], "none"))
def test_unknown(self):
matches, kind = mrp.resolve_ref(self.rows, "nope-missing")
self.assertEqual((matches, kind), ([], "none"))
def test_ambiguous_prefix(self):
matches, kind = mrp.resolve_ref(self.rows, "01a113f1")
self.assertEqual(len(matches), 2)
self.assertEqual(kind, "prefix")
class TestGuard(unittest.TestCase):
def setUp(self):
self.rows = mrp.annotate_rows([
make_row("01a113f1" + "0" * 24, WS_NETVM, name="alder-halo"),
make_row("f" * 32, WS_HOME, name="far-away"),
], make_auth(active="muse", mapping={
"01a113f1" + "0" * 24: "def",
"f" * 32: "muse",
}))
def test_wrong_profile_blocked_with_fix(self):
d = mrp.check_resume(self.rows, "alder-halo", WS_NETVM,
make_auth(active="muse", mapping={
"01a113f1" + "0" * 24: "def"}))
self.assertFalse(d["ok"])
self.assertEqual(d["reason"], "wrong-profile")
self.assertIn("muse-auth use def", d["fix"])
def test_matching_profile_ok(self):
auth = make_auth(active="muse", mapping={"01a113f1" + "0" * 24: "muse"})
rows = mrp.annotate_rows(
[make_row("01a113f1" + "0" * 24, WS_NETVM, name="alder-halo")], auth)
d = mrp.check_resume(rows, "alder-halo", WS_NETVM, auth)
self.assertTrue(d["ok"])
self.assertEqual(d["reason"], "ok")
def test_wrong_workspace_blocked(self):
auth = make_auth(active="muse", mapping={"f" * 32: "muse"})
rows = mrp.annotate_rows([make_row("f" * 32, WS_HOME, name="far-away")], auth)
d = mrp.check_resume(rows, "far-away", WS_NETVM, auth)
self.assertFalse(d["ok"])
self.assertEqual(d["reason"], "wrong-workspace")
self.assertIn("cd", d["fix"])
def test_unknown_session(self):
d = mrp.check_resume(self.rows, "ghost-nope", WS_NETVM, make_auth())
self.assertFalse(d["ok"])
self.assertEqual(d["reason"], "unknown-session")
def test_bad_status_blocked(self):
auth = make_auth(active="muse")
rows = mrp.annotate_rows(
[make_row("d" * 32, WS_NETVM, name="stale-one", status="deleted")], auth)
d = mrp.check_resume(rows, "stale-one", WS_NETVM, auth)
self.assertFalse(d["ok"])
self.assertEqual(d["reason"], "bad-status")
def test_unknown_profile_warns_not_blocks(self):
auth = make_auth(readable=False)
rows = mrp.annotate_rows(
[make_row("e" * 32, WS_NETVM, name="sandboxed-one")], auth)
d = mrp.check_resume(rows, "sandboxed-one", WS_NETVM, auth)
self.assertTrue(d["ok"])
self.assertIn("unverified", d["detail"].lower())
class TestIndexAndLogSources(unittest.TestCase):
def test_load_index_rows_read_only(self):
with tempfile.TemporaryDirectory() as tmp:
db_path = os.path.join(tmp, "session-index.db")
conn = sqlite3.connect(db_path)
conn.execute(
"CREATE TABLE sessions (session_id TEXT PRIMARY KEY,"
" session_name TEXT, workspace_root TEXT, workspace_key TEXT,"
" provider_id TEXT, model_id TEXT, git_branch TEXT, title TEXT,"
" first_user_prompt TEXT, created_at_us INTEGER,"
" updated_at_us INTEGER, prompt_count INTEGER, status TEXT)"
)
older, newer = make_row("a" * 32, WS_NETVM, name="old",
updated_us=1791000000000000), \
make_row("b" * 32, WS_NETVM, name="new",
updated_us=1791300000000000)
for r in (older, newer):
conn.execute(
"INSERT INTO sessions VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)",
tuple(r[c] for c in mrp.INDEX_COLUMNS),
)
conn.commit()
conn.close()
rows = mrp.load_index_rows(db_path)
self.assertEqual([r["session_name"] for r in rows], ["new", "old"])
def test_missing_index_returns_none(self):
self.assertIsNone(mrp.load_index_rows("/nonexistent/x.db"))
def test_log_scan_fallback_extracts_workspace_and_name(self):
with tempfile.TemporaryDirectory() as tmp:
session_id = "9" * 32
sdir = os.path.join(tmp, "2026", "10", "07", session_id)
os.makedirs(sdir)
records = [
{"payload_type": "runtime.session.metadata",
"recorded_at": 1791000000000000,
"payload": {"record": {"workspace_root": WS_NETVM}}},
{"payload_type": "session.name.changed",
"recorded_at": 1791000001000000,
"payload": {"new_name": "scanned-one"}},
{"payload_type": "runtime.session",
"recorded_at": 1791000002000000,
"payload": {"event": {"kind": "started", "prompt": "hello world"}}},
]
with open(os.path.join(sdir, "session.jsonl"), "w") as fh:
for rec in records:
fh.write(json.dumps(rec) + "\n")
rows = mrp.scan_session_logs(tmp)
self.assertEqual(len(rows), 1)
row = rows[0]
self.assertEqual(row["session_id"], session_id)
self.assertEqual(row["workspace_key"], WS_NETVM)
self.assertEqual(row["session_name"], "scanned-one")
self.assertEqual(row["prompt_count"], 1)
def test_auth_state_never_reads_auth_json(self):
with tempfile.TemporaryDirectory() as tmp:
with open(os.path.join(tmp, "active_profile"), "w") as fh:
fh.write("muse\n")
with open(os.path.join(tmp, "session_profiles.json"), "w") as fh:
json.dump({"s1": "muse"}, fh)
with open(os.path.join(tmp, "switch_history.jsonl"), "w") as fh:
fh.write(json.dumps({"profile": "muse", "epoch": 1.0}) + "\n")
# A decoy auth.json with secret bytes must not be opened: make it
# unreadable and assert load still succeeds without error.
decoy = os.path.join(tmp, "auth.json")
with open(decoy, "w") as fh:
fh.write('{"secret": true}')
os.chmod(decoy, 0)
try:
state = mrp.load_auth_state(tmp)
finally:
os.chmod(decoy, 0o600)
self.assertTrue(state["readable"])
self.assertEqual(state["active"], "muse")
self.assertEqual(state["session_profiles"], {"s1": "muse"})
if __name__ == "__main__":
unittest.main()
+241
View File
@@ -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()