compartment 4.2__tar.gz → 4.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {compartment-4.2/src/compartment.egg-info → compartment-4.3}/PKG-INFO +1 -1
- {compartment-4.2 → compartment-4.3}/pyproject.toml +1 -1
- {compartment-4.2 → compartment-4.3}/src/compartment/__init__.py +1 -1
- {compartment-4.2 → compartment-4.3}/src/compartment/menubar.py +63 -10
- {compartment-4.2 → compartment-4.3}/src/compartment/systray.py +66 -4
- {compartment-4.2 → compartment-4.3/src/compartment.egg-info}/PKG-INFO +1 -1
- {compartment-4.2 → compartment-4.3}/src/compartment.egg-info/SOURCES.txt +1 -0
- {compartment-4.2 → compartment-4.3}/tests/test_attempt_pacing.py +10 -1
- compartment-4.3/tests/test_stays_running.py +169 -0
- {compartment-4.2 → compartment-4.3}/LICENSE +0 -0
- {compartment-4.2 → compartment-4.3}/README.md +0 -0
- {compartment-4.2 → compartment-4.3}/setup.cfg +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/acl.py +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/agent_skill.py +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/audit.py +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/bench.py +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/claude_desktop.py +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/claude_hooks.py +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/claude_memory.py +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/cli.py +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/clients.py +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/crypto.py +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/dash.py +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/data/agent-skill/SKILL.md +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/data/app.icns +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/data/app.png +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/data/hermes-plugin/__init__.py +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/data/hermes-plugin/plugin.yaml +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/data/menubar.png +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/data/menubar@2x.png +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/data/starter.mpack +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/data/tray.ico +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/embed.py +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/home.py +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/longmemeval.py +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/models/bge-small-en-v1.5-int8/model_quantized.onnx +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/models/bge-small-en-v1.5-int8/tokenizer.json +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/offline_guard.py +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/packs.py +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/platforms.py +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/ranking.py +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/salience.py +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/selftest.py +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/server.py +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/session.py +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/store.py +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/vault.py +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/vaultfile.py +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/vindex.py +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment/wire.py +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment.egg-info/dependency_links.txt +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment.egg-info/entry_points.txt +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment.egg-info/requires.txt +0 -0
- {compartment-4.2 → compartment-4.3}/src/compartment.egg-info/top_level.txt +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_acl_audit_packs.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_acl_precedence.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_agent_skill.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_audit_chain.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_autoconnect.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_claude_desktop.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_claude_hooks_and_integrate.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_claude_memory_and_recent.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_clients.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_connect_buttons.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_connect_environment.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_crypto.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_dash.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_instructions.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_journal_framing.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_linux_panel.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_login_item_identity.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_long_memory_recall.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_macos_bundle.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_menubar.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_offline_guard_paths.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_offline_selftest_bench.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_ondisk_format.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_pack_trust.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_platforms.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_ranking.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_relations.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_salience.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_session_entropy.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_session_lock.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_single_instance.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_starter_data_quality.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_starter_packs.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_starter_visibility.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_status_bar_install.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_systray.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_tamper.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_twofa.py +0 -0
- {compartment-4.2 → compartment-4.3}/tests/test_vault_ops.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "compartment"
|
|
7
|
-
version = "4.
|
|
7
|
+
version = "4.3"
|
|
8
8
|
description = "Compartment - high-security, fully offline, encrypted vector memory for AI agents (MCP)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "Apache-2.0" }
|
|
@@ -664,11 +664,12 @@ def _launcher_argv() -> list[str]:
|
|
|
664
664
|
def _set_login_agent(enabled: bool) -> str:
|
|
665
665
|
plist = _agent_plist()
|
|
666
666
|
if not enabled:
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
667
|
+
# bootout first: with KeepAlive set, a running copy that is still
|
|
668
|
+
# registered would be restarted by launchd after the plist was
|
|
669
|
+
# deleted, and the icon would come back from a file that no longer
|
|
670
|
+
# exists. Unload stays as the fallback for older macOS.
|
|
671
|
+
_launchctl("bootout", f"{_gui_domain()}/{LAUNCH_AGENT_LABEL}")
|
|
672
|
+
_launchctl("unload", str(plist))
|
|
672
673
|
plist.unlink(missing_ok=True)
|
|
673
674
|
return "off"
|
|
674
675
|
argv = _launcher_argv()
|
|
@@ -684,15 +685,67 @@ def _set_login_agent(enabled: bool) -> str:
|
|
|
684
685
|
' <key>ProgramArguments</key>\n'
|
|
685
686
|
f' <array>\n{args} </array>\n'
|
|
686
687
|
' <key>RunAtLoad</key><true/>\n'
|
|
687
|
-
|
|
688
|
+
# KeepAlive was <false/>, which made this a one-shot: launchd started
|
|
689
|
+
# the app at login and then never looked at it again. Anything that
|
|
690
|
+
# ended the process - a crash, an ObjC exception, macOS reclaiming
|
|
691
|
+
# memory, an upgrade replacing the binary underneath it - took the
|
|
692
|
+
# icon out of the menu bar until the next login, with nothing said.
|
|
693
|
+
# A status bar app is meant to sit there for as long as the machine
|
|
694
|
+
# is up.
|
|
695
|
+
#
|
|
696
|
+
# SuccessfulExit false is the form that means "bring it back if it
|
|
697
|
+
# died, leave it alone if it left". Quit calls NSApp.terminate_, which
|
|
698
|
+
# exits 0, so the one way to remove the icon is still the one the user
|
|
699
|
+
# asked for. A crash exits non-zero and comes straight back.
|
|
700
|
+
' <key>KeepAlive</key>\n'
|
|
701
|
+
' <dict>\n'
|
|
702
|
+
' <key>SuccessfulExit</key><false/>\n'
|
|
703
|
+
' </dict>\n'
|
|
688
704
|
' </dict>\n'
|
|
689
705
|
'</plist>\n', encoding="utf-8")
|
|
706
|
+
return _bootstrap_agent(plist)
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def _launchctl(*argv: str) -> tuple[int, str]:
|
|
710
|
+
"""Run launchctl and actually look at what it said."""
|
|
690
711
|
try:
|
|
691
|
-
subprocess.run(["launchctl",
|
|
692
|
-
|
|
712
|
+
r = subprocess.run(["launchctl", *argv], capture_output=True,
|
|
713
|
+
text=True, timeout=15)
|
|
693
714
|
except (OSError, subprocess.SubprocessError) as exc:
|
|
694
|
-
return
|
|
695
|
-
return "
|
|
715
|
+
return 1, str(exc)
|
|
716
|
+
return r.returncode, (r.stderr or r.stdout or "").strip()
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def _gui_domain() -> str:
|
|
720
|
+
"""launchd's per-user domain. `os.getuid` is POSIX-only and this module
|
|
721
|
+
is imported on every platform, so it is asked for rather than assumed:
|
|
722
|
+
an AttributeError here would take down anything that merely touched the
|
|
723
|
+
login-agent code on Windows."""
|
|
724
|
+
uid = getattr(os, "getuid", None)
|
|
725
|
+
return f"gui/{uid() if uid else 0}"
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def _bootstrap_agent(plist: Path) -> str:
|
|
729
|
+
"""Load the agent, and report a refusal as one.
|
|
730
|
+
|
|
731
|
+
The old code ran `launchctl load` and threw the result away, so a plist
|
|
732
|
+
that was written but never loaded reported "on" and started nothing at
|
|
733
|
+
the next login - which is exactly the state a machine ends up in when
|
|
734
|
+
the label is already bootstrapped and load quietly refuses.
|
|
735
|
+
|
|
736
|
+
`bootstrap` is the modern verb and the only one that reports properly.
|
|
737
|
+
An existing registration is booted out first so this is idempotent, and
|
|
738
|
+
`load` remains as the fallback for macOS versions that predate it.
|
|
739
|
+
"""
|
|
740
|
+
domain = _gui_domain()
|
|
741
|
+
_launchctl("bootout", f"{domain}/{LAUNCH_AGENT_LABEL}") # ok if absent
|
|
742
|
+
code, msg = _launchctl("bootstrap", domain, str(plist))
|
|
743
|
+
if code == 0:
|
|
744
|
+
return "on"
|
|
745
|
+
code, msg2 = _launchctl("load", "-w", str(plist)) # older macOS
|
|
746
|
+
if code == 0:
|
|
747
|
+
return "on"
|
|
748
|
+
return f"failed: {msg or msg2 or 'launchctl refused the agent'}"
|
|
696
749
|
|
|
697
750
|
|
|
698
751
|
def login_status() -> str:
|
|
@@ -223,6 +223,52 @@ def remove_desktop_entry() -> bool:
|
|
|
223
223
|
return False
|
|
224
224
|
|
|
225
225
|
|
|
226
|
+
# --- starting at login on Linux ---------------------------------------------
|
|
227
|
+
# An entry in the applications menu makes Compartment findable. It does not
|
|
228
|
+
# make it run, and the two were being treated as one thing: `login_status`
|
|
229
|
+
# answered "on" whenever the menu entry existed, so a desktop that had never
|
|
230
|
+
# started Compartment at login reported that it did. The autostart directory
|
|
231
|
+
# is a different directory with a different meaning, and this is it.
|
|
232
|
+
|
|
233
|
+
def autostart_entry_path() -> Path:
|
|
234
|
+
base = os.environ.get("XDG_CONFIG_HOME") or str(Path.home() / ".config")
|
|
235
|
+
return Path(base) / "autostart" / DESKTOP_FILE
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def install_autostart_entry(vault: str | None = None) -> str:
|
|
239
|
+
"""Start Compartment at login, the way every XDG desktop reads it."""
|
|
240
|
+
path = autostart_entry_path()
|
|
241
|
+
try:
|
|
242
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
243
|
+
icon = app_icon_path()
|
|
244
|
+
path.write_text(
|
|
245
|
+
"[Desktop Entry]\n"
|
|
246
|
+
"Type=Application\n"
|
|
247
|
+
"Name=Compartment\n"
|
|
248
|
+
"Comment=Encrypted memory for AI agents\n"
|
|
249
|
+
f"Exec={_panel_command(vault)}\n"
|
|
250
|
+
+ (f"Icon={icon}\n" if icon.is_file() else "")
|
|
251
|
+
+ "Terminal=false\n"
|
|
252
|
+
# GNOME and KDE both read these, and without them a session can
|
|
253
|
+
# decide an autostart entry is stale and skip it.
|
|
254
|
+
"X-GNOME-Autostart-enabled=true\n"
|
|
255
|
+
"Hidden=false\n"
|
|
256
|
+
"NoDisplay=false\n",
|
|
257
|
+
encoding="utf-8")
|
|
258
|
+
path.chmod(0o644)
|
|
259
|
+
except OSError as exc:
|
|
260
|
+
return f"error: {exc}"
|
|
261
|
+
return str(path)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def remove_autostart_entry() -> bool:
|
|
265
|
+
try:
|
|
266
|
+
autostart_entry_path().unlink(missing_ok=True)
|
|
267
|
+
return True
|
|
268
|
+
except OSError:
|
|
269
|
+
return False
|
|
270
|
+
|
|
271
|
+
|
|
226
272
|
def _is_linux() -> bool:
|
|
227
273
|
"""This module draws the panel on Windows and on everything that is not
|
|
228
274
|
macOS. Only the latter uses desktop entries, and macOS must never get
|
|
@@ -233,9 +279,10 @@ def _is_linux() -> bool:
|
|
|
233
279
|
|
|
234
280
|
def login_status() -> str:
|
|
235
281
|
if _is_linux():
|
|
236
|
-
#
|
|
237
|
-
#
|
|
238
|
-
|
|
282
|
+
# The autostart entry, not the applications-menu entry. Reading the
|
|
283
|
+
# menu entry meant this said "on" for a machine that had never
|
|
284
|
+
# started Compartment at login in its life.
|
|
285
|
+
return "on" if autostart_entry_path().is_file() else "off"
|
|
239
286
|
try:
|
|
240
287
|
winreg = _winreg()
|
|
241
288
|
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, RUN_KEY) as k:
|
|
@@ -251,9 +298,15 @@ def set_login(enabled: bool) -> str:
|
|
|
251
298
|
"""Register or drop the Run entry. Returns what actually happened."""
|
|
252
299
|
if _is_linux():
|
|
253
300
|
if not enabled:
|
|
301
|
+
remove_autostart_entry()
|
|
254
302
|
return "off" if remove_desktop_entry() else "error"
|
|
303
|
+
# Both, and they are not the same thing: the menu entry is how the
|
|
304
|
+
# app is found, the autostart entry is how it comes up at login.
|
|
255
305
|
out = install_desktop_entry()
|
|
256
|
-
|
|
306
|
+
if out.startswith("error"):
|
|
307
|
+
return out
|
|
308
|
+
auto = install_autostart_entry()
|
|
309
|
+
return "on" if not auto.startswith("error") else auto
|
|
257
310
|
if sys.platform != "win32":
|
|
258
311
|
return "error: start at login is handled by the menu bar app here"
|
|
259
312
|
try:
|
|
@@ -262,6 +315,13 @@ def set_login(enabled: bool) -> str:
|
|
|
262
315
|
if enabled:
|
|
263
316
|
winreg.SetValueEx(k, RUN_VALUE, 0, winreg.REG_SZ,
|
|
264
317
|
_autostart_command())
|
|
318
|
+
# Read it back. A write that the registry accepted and then
|
|
319
|
+
# dropped - a policy, a locked hive, a roaming profile - used
|
|
320
|
+
# to report "on" and start nothing at the next sign-in.
|
|
321
|
+
try:
|
|
322
|
+
winreg.QueryValueEx(k, RUN_VALUE)
|
|
323
|
+
except FileNotFoundError:
|
|
324
|
+
return "error: the Run entry did not survive the write"
|
|
265
325
|
return "on"
|
|
266
326
|
try:
|
|
267
327
|
winreg.DeleteValue(k, RUN_VALUE)
|
|
@@ -718,4 +778,6 @@ def quit_running() -> bool:
|
|
|
718
778
|
|
|
719
779
|
__all__ = ["run", "self_check", "login_status", "set_login", "quit_running",
|
|
720
780
|
"panel_rows",
|
|
781
|
+
"autostart_entry_path", "install_autostart_entry",
|
|
782
|
+
"remove_autostart_entry",
|
|
721
783
|
"icon_path"]
|
|
@@ -78,12 +78,21 @@ def test_no_lockout_the_right_passphrase_still_opens_after_many_failures(
|
|
|
78
78
|
|
|
79
79
|
def test_a_correct_attempt_clears_the_pacing(vault_path, fast):
|
|
80
80
|
Vault.create(vault_path, PASS, creator="t").lock()
|
|
81
|
+
# What an unlock costs with nothing pacing it. Argon2 and opening the
|
|
82
|
+
# file are not free, and on a loaded CI runner they can take longer than
|
|
83
|
+
# the whole test interval, so "no wait was added" has to be measured
|
|
84
|
+
# against that cost rather than against a flat number. Comparing to 0.4s
|
|
85
|
+
# made this a test of how busy the machine was.
|
|
86
|
+
start = time.monotonic()
|
|
87
|
+
Vault.unlock(vault_path, passphrase=PASS).lock()
|
|
88
|
+
baseline = time.monotonic() - start
|
|
89
|
+
|
|
81
90
|
with pytest.raises((TamperError, CryptoError)):
|
|
82
91
|
Vault.unlock(vault_path, passphrase="wrong")
|
|
83
92
|
Vault.unlock(vault_path, passphrase=PASS).lock() # pays the wait once
|
|
84
93
|
start = time.monotonic()
|
|
85
94
|
Vault.unlock(vault_path, passphrase=PASS).lock() # and not again
|
|
86
|
-
assert time.monotonic() - start < fast
|
|
95
|
+
assert time.monotonic() - start < baseline + fast
|
|
87
96
|
|
|
88
97
|
|
|
89
98
|
def test_the_user_is_told_why_it_is_waiting(vault_path, fast, capsys):
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""The icon is supposed to be there until somebody quits it.
|
|
2
|
+
|
|
3
|
+
A status bar app is not a program you launch, it is a program that is simply
|
|
4
|
+
there, like the clock and the battery. Every fault covered here ended the same
|
|
5
|
+
way for the user: the icon was gone, nothing said why, and the fix was to know
|
|
6
|
+
a command. So these are about persistence rather than appearance.
|
|
7
|
+
|
|
8
|
+
- macOS started the app once at login and then stopped caring. Anything
|
|
9
|
+
that ended the process took the icon away until the next login.
|
|
10
|
+
- macOS also reported success for an agent it had failed to load, so an
|
|
11
|
+
install could say "start at login: on" and start nothing.
|
|
12
|
+
- Linux answered the same question by looking at the applications menu,
|
|
13
|
+
which says whether Compartment can be found, not whether it runs.
|
|
14
|
+
- Windows wrote a Run entry without ever reading it back.
|
|
15
|
+
"""
|
|
16
|
+
import plistlib
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
import pytest
|
|
20
|
+
|
|
21
|
+
from compartment import menubar, systray
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ------------------------------------------------------------------- macOS
|
|
25
|
+
|
|
26
|
+
def _plist(monkeypatch, tmp_path):
|
|
27
|
+
"""Write the login agent into a temp home and hand back the parsed dict."""
|
|
28
|
+
monkeypatch.setattr(menubar.Path, "home", classmethod(lambda cls: tmp_path))
|
|
29
|
+
monkeypatch.setattr(menubar, "_launcher_argv", lambda: ["/bin/true"])
|
|
30
|
+
calls = []
|
|
31
|
+
monkeypatch.setattr(menubar, "_launchctl",
|
|
32
|
+
lambda *a: (calls.append(a), (0, ""))[1])
|
|
33
|
+
out = menubar._set_login_agent(True)
|
|
34
|
+
path = tmp_path / "Library" / "LaunchAgents" / f"{menubar.LAUNCH_AGENT_LABEL}.plist"
|
|
35
|
+
return out, plistlib.loads(path.read_bytes()), calls, path
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_launchd_brings_the_icon_back_after_a_crash(monkeypatch, tmp_path):
|
|
39
|
+
_out, data, _calls, _p = _plist(monkeypatch, tmp_path)
|
|
40
|
+
# Not False, and not True either. False is the one-shot that lost the
|
|
41
|
+
# icon on any unexpected exit; plain True would fight the Quit button.
|
|
42
|
+
assert data["KeepAlive"] == {"SuccessfulExit": False}
|
|
43
|
+
assert data["RunAtLoad"] is True
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_quitting_on_purpose_is_still_allowed_to_win(monkeypatch, tmp_path):
|
|
47
|
+
"""SuccessfulExit false means launchd relaunches a non-zero exit only.
|
|
48
|
+
Quit goes through NSApp.terminate_, which exits zero, so the icon stays
|
|
49
|
+
gone until the user asks for it back."""
|
|
50
|
+
_out, data, _calls, _p = _plist(monkeypatch, tmp_path)
|
|
51
|
+
assert data["KeepAlive"]["SuccessfulExit"] is False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_the_agent_is_actually_loaded_not_just_written(monkeypatch, tmp_path):
|
|
55
|
+
_out, _data, calls, path = _plist(monkeypatch, tmp_path)
|
|
56
|
+
verbs = [c[0] for c in calls]
|
|
57
|
+
assert "bootstrap" in verbs, calls
|
|
58
|
+
# Booted out first, so re-running is idempotent rather than a refusal.
|
|
59
|
+
assert verbs.index("bootout") < verbs.index("bootstrap")
|
|
60
|
+
assert any(str(path) in c for c in calls)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_a_refused_agent_is_reported_as_a_failure(monkeypatch, tmp_path):
|
|
64
|
+
"""The old code ran `launchctl load`, discarded the result and returned
|
|
65
|
+
"on", so an agent that never loaded looked wired."""
|
|
66
|
+
monkeypatch.setattr(menubar.Path, "home", classmethod(lambda cls: tmp_path))
|
|
67
|
+
monkeypatch.setattr(menubar, "_launcher_argv", lambda: ["/bin/true"])
|
|
68
|
+
monkeypatch.setattr(menubar, "_launchctl",
|
|
69
|
+
lambda *a: (1, "Load failed: 5: Input/output error"))
|
|
70
|
+
out = menubar._set_login_agent(True)
|
|
71
|
+
assert out.startswith("failed"), out
|
|
72
|
+
assert "Input/output error" in out
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_turning_it_off_deregisters_before_deleting(monkeypatch, tmp_path):
|
|
76
|
+
"""With KeepAlive set, deleting the plist while the job is still
|
|
77
|
+
registered would have launchd relaunch it from a file that is gone."""
|
|
78
|
+
monkeypatch.setattr(menubar.Path, "home", classmethod(lambda cls: tmp_path))
|
|
79
|
+
monkeypatch.setattr(menubar, "_launcher_argv", lambda: ["/bin/true"])
|
|
80
|
+
monkeypatch.setattr(menubar, "_launchctl", lambda *a: (0, ""))
|
|
81
|
+
menubar._set_login_agent(True)
|
|
82
|
+
|
|
83
|
+
calls = []
|
|
84
|
+
monkeypatch.setattr(menubar, "_launchctl",
|
|
85
|
+
lambda *a: (calls.append(a), (0, ""))[1])
|
|
86
|
+
assert menubar._set_login_agent(False) == "off"
|
|
87
|
+
assert calls and calls[0][0] == "bootout"
|
|
88
|
+
path = tmp_path / "Library" / "LaunchAgents" / f"{menubar.LAUNCH_AGENT_LABEL}.plist"
|
|
89
|
+
assert not path.exists()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ------------------------------------------------------------------- Linux
|
|
93
|
+
|
|
94
|
+
def test_linux_autostart_is_a_different_file_from_the_menu_entry(monkeypatch,
|
|
95
|
+
tmp_path):
|
|
96
|
+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "cfg"))
|
|
97
|
+
monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path / "data"))
|
|
98
|
+
auto = systray.autostart_entry_path()
|
|
99
|
+
menu = systray.desktop_entry_path()
|
|
100
|
+
assert auto != menu
|
|
101
|
+
assert auto.parent.name == "autostart"
|
|
102
|
+
assert menu.parent.name == "applications"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_linux_login_status_reads_autostart_not_the_menu(monkeypatch, tmp_path):
|
|
106
|
+
"""The bug: an applications-menu entry made this report "on" for a
|
|
107
|
+
machine that had never started Compartment at login."""
|
|
108
|
+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "cfg"))
|
|
109
|
+
monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path / "data"))
|
|
110
|
+
monkeypatch.setattr(systray, "_is_linux", lambda: True)
|
|
111
|
+
|
|
112
|
+
systray.install_desktop_entry("/v/memory.vault") # findable...
|
|
113
|
+
assert systray.login_status() == "off" # ...but not running
|
|
114
|
+
|
|
115
|
+
systray.install_autostart_entry("/v/memory.vault")
|
|
116
|
+
assert systray.login_status() == "on"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_linux_autostart_entry_is_one_desktops_will_honour(monkeypatch,
|
|
120
|
+
tmp_path):
|
|
121
|
+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "cfg"))
|
|
122
|
+
systray.install_autostart_entry("/v/memory.vault")
|
|
123
|
+
text = systray.autostart_entry_path().read_text()
|
|
124
|
+
assert text.startswith("[Desktop Entry]")
|
|
125
|
+
assert "Type=Application" in text
|
|
126
|
+
# Without these a session is entitled to treat the entry as stale.
|
|
127
|
+
assert "X-GNOME-Autostart-enabled=true" in text
|
|
128
|
+
assert "Hidden=false" in text
|
|
129
|
+
assert "/v/memory.vault" in text
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_linux_turning_it_off_removes_the_autostart_entry(monkeypatch,
|
|
133
|
+
tmp_path):
|
|
134
|
+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "cfg"))
|
|
135
|
+
monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path / "data"))
|
|
136
|
+
monkeypatch.setattr(systray, "_is_linux", lambda: True)
|
|
137
|
+
systray.set_login(True)
|
|
138
|
+
assert systray.login_status() == "on"
|
|
139
|
+
systray.set_login(False)
|
|
140
|
+
assert systray.login_status() == "off"
|
|
141
|
+
assert not systray.autostart_entry_path().exists()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_linux_set_login_installs_both(monkeypatch, tmp_path):
|
|
145
|
+
"""Turning on start-at-login must not cost the applications-menu entry
|
|
146
|
+
that was there before it."""
|
|
147
|
+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "cfg"))
|
|
148
|
+
monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path / "data"))
|
|
149
|
+
monkeypatch.setattr(systray, "_is_linux", lambda: True)
|
|
150
|
+
assert systray.set_login(True) == "on"
|
|
151
|
+
assert systray.autostart_entry_path().is_file()
|
|
152
|
+
assert systray.desktop_entry_path().is_file()
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ----------------------------------------------------------------- Windows
|
|
156
|
+
|
|
157
|
+
@pytest.mark.skipif(sys.platform != "win32", reason="Run key is Windows-only")
|
|
158
|
+
def test_windows_reads_the_run_entry_back(monkeypatch):
|
|
159
|
+
"""A write the registry accepted and then dropped used to report "on"."""
|
|
160
|
+
import winreg
|
|
161
|
+
real_query = winreg.QueryValueEx
|
|
162
|
+
|
|
163
|
+
def vanished(key, name):
|
|
164
|
+
if name == systray.RUN_VALUE:
|
|
165
|
+
raise FileNotFoundError(name)
|
|
166
|
+
return real_query(key, name)
|
|
167
|
+
|
|
168
|
+
monkeypatch.setattr(winreg, "QueryValueEx", vanished)
|
|
169
|
+
assert systray.set_login(True).startswith("error")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{compartment-4.2 → compartment-4.3}/src/compartment/models/bge-small-en-v1.5-int8/tokenizer.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|