compartment 3.3__tar.gz → 3.4__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.
Files changed (74) hide show
  1. {compartment-3.3 → compartment-3.4}/PKG-INFO +6 -1
  2. {compartment-3.3 → compartment-3.4}/README.md +5 -0
  3. {compartment-3.3 → compartment-3.4}/pyproject.toml +1 -1
  4. {compartment-3.3 → compartment-3.4}/src/compartment/__init__.py +1 -1
  5. {compartment-3.3 → compartment-3.4}/src/compartment/menubar.py +65 -0
  6. {compartment-3.3 → compartment-3.4}/src/compartment/systray.py +40 -6
  7. {compartment-3.3 → compartment-3.4}/src/compartment.egg-info/PKG-INFO +6 -1
  8. {compartment-3.3 → compartment-3.4}/src/compartment.egg-info/SOURCES.txt +1 -0
  9. compartment-3.4/tests/test_connect_buttons.py +165 -0
  10. {compartment-3.3 → compartment-3.4}/tests/test_systray.py +2 -1
  11. {compartment-3.3 → compartment-3.4}/setup.cfg +0 -0
  12. {compartment-3.3 → compartment-3.4}/src/compartment/acl.py +0 -0
  13. {compartment-3.3 → compartment-3.4}/src/compartment/audit.py +0 -0
  14. {compartment-3.3 → compartment-3.4}/src/compartment/bench.py +0 -0
  15. {compartment-3.3 → compartment-3.4}/src/compartment/claude_hooks.py +0 -0
  16. {compartment-3.3 → compartment-3.4}/src/compartment/claude_memory.py +0 -0
  17. {compartment-3.3 → compartment-3.4}/src/compartment/cli.py +0 -0
  18. {compartment-3.3 → compartment-3.4}/src/compartment/crypto.py +0 -0
  19. {compartment-3.3 → compartment-3.4}/src/compartment/dash.py +0 -0
  20. {compartment-3.3 → compartment-3.4}/src/compartment/data/hermes-plugin/__init__.py +0 -0
  21. {compartment-3.3 → compartment-3.4}/src/compartment/data/hermes-plugin/plugin.yaml +0 -0
  22. {compartment-3.3 → compartment-3.4}/src/compartment/data/menubar.png +0 -0
  23. {compartment-3.3 → compartment-3.4}/src/compartment/data/menubar@2x.png +0 -0
  24. {compartment-3.3 → compartment-3.4}/src/compartment/data/starter.mpack +0 -0
  25. {compartment-3.3 → compartment-3.4}/src/compartment/data/tray.ico +0 -0
  26. {compartment-3.3 → compartment-3.4}/src/compartment/embed.py +0 -0
  27. {compartment-3.3 → compartment-3.4}/src/compartment/home.py +0 -0
  28. {compartment-3.3 → compartment-3.4}/src/compartment/longmemeval.py +0 -0
  29. {compartment-3.3 → compartment-3.4}/src/compartment/models/bge-small-en-v1.5-int8/model_quantized.onnx +0 -0
  30. {compartment-3.3 → compartment-3.4}/src/compartment/models/bge-small-en-v1.5-int8/tokenizer.json +0 -0
  31. {compartment-3.3 → compartment-3.4}/src/compartment/offline_guard.py +0 -0
  32. {compartment-3.3 → compartment-3.4}/src/compartment/packs.py +0 -0
  33. {compartment-3.3 → compartment-3.4}/src/compartment/platforms.py +0 -0
  34. {compartment-3.3 → compartment-3.4}/src/compartment/salience.py +0 -0
  35. {compartment-3.3 → compartment-3.4}/src/compartment/selftest.py +0 -0
  36. {compartment-3.3 → compartment-3.4}/src/compartment/server.py +0 -0
  37. {compartment-3.3 → compartment-3.4}/src/compartment/session.py +0 -0
  38. {compartment-3.3 → compartment-3.4}/src/compartment/store.py +0 -0
  39. {compartment-3.3 → compartment-3.4}/src/compartment/vault.py +0 -0
  40. {compartment-3.3 → compartment-3.4}/src/compartment/vaultfile.py +0 -0
  41. {compartment-3.3 → compartment-3.4}/src/compartment/vindex.py +0 -0
  42. {compartment-3.3 → compartment-3.4}/src/compartment/wire.py +0 -0
  43. {compartment-3.3 → compartment-3.4}/src/compartment.egg-info/dependency_links.txt +0 -0
  44. {compartment-3.3 → compartment-3.4}/src/compartment.egg-info/entry_points.txt +0 -0
  45. {compartment-3.3 → compartment-3.4}/src/compartment.egg-info/requires.txt +0 -0
  46. {compartment-3.3 → compartment-3.4}/src/compartment.egg-info/top_level.txt +0 -0
  47. {compartment-3.3 → compartment-3.4}/tests/test_acl_audit_packs.py +0 -0
  48. {compartment-3.3 → compartment-3.4}/tests/test_acl_precedence.py +0 -0
  49. {compartment-3.3 → compartment-3.4}/tests/test_attempt_pacing.py +0 -0
  50. {compartment-3.3 → compartment-3.4}/tests/test_audit_chain.py +0 -0
  51. {compartment-3.3 → compartment-3.4}/tests/test_claude_hooks_and_integrate.py +0 -0
  52. {compartment-3.3 → compartment-3.4}/tests/test_claude_memory_and_recent.py +0 -0
  53. {compartment-3.3 → compartment-3.4}/tests/test_crypto.py +0 -0
  54. {compartment-3.3 → compartment-3.4}/tests/test_dash.py +0 -0
  55. {compartment-3.3 → compartment-3.4}/tests/test_instructions.py +0 -0
  56. {compartment-3.3 → compartment-3.4}/tests/test_journal_framing.py +0 -0
  57. {compartment-3.3 → compartment-3.4}/tests/test_macos_bundle.py +0 -0
  58. {compartment-3.3 → compartment-3.4}/tests/test_menubar.py +0 -0
  59. {compartment-3.3 → compartment-3.4}/tests/test_offline_guard_paths.py +0 -0
  60. {compartment-3.3 → compartment-3.4}/tests/test_offline_selftest_bench.py +0 -0
  61. {compartment-3.3 → compartment-3.4}/tests/test_ondisk_format.py +0 -0
  62. {compartment-3.3 → compartment-3.4}/tests/test_pack_trust.py +0 -0
  63. {compartment-3.3 → compartment-3.4}/tests/test_platforms.py +0 -0
  64. {compartment-3.3 → compartment-3.4}/tests/test_relations.py +0 -0
  65. {compartment-3.3 → compartment-3.4}/tests/test_salience.py +0 -0
  66. {compartment-3.3 → compartment-3.4}/tests/test_session_entropy.py +0 -0
  67. {compartment-3.3 → compartment-3.4}/tests/test_session_lock.py +0 -0
  68. {compartment-3.3 → compartment-3.4}/tests/test_starter_data_quality.py +0 -0
  69. {compartment-3.3 → compartment-3.4}/tests/test_starter_packs.py +0 -0
  70. {compartment-3.3 → compartment-3.4}/tests/test_starter_visibility.py +0 -0
  71. {compartment-3.3 → compartment-3.4}/tests/test_status_bar_install.py +0 -0
  72. {compartment-3.3 → compartment-3.4}/tests/test_tamper.py +0 -0
  73. {compartment-3.3 → compartment-3.4}/tests/test_twofa.py +0 -0
  74. {compartment-3.3 → compartment-3.4}/tests/test_vault_ops.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: compartment
3
- Version: 3.3
3
+ Version: 3.4
4
4
  Summary: Compartment - high-security, fully offline, encrypted vector memory for AI agents (MCP)
5
5
  Project-URL: Homepage, https://github.com/MaxFreedomPollard/Compartment
6
6
  Project-URL: Source, https://github.com/MaxFreedomPollard/Compartment
@@ -243,6 +243,11 @@ network at all.
243
243
  One command per platform. Each installs the package, creates your
244
244
  encrypted vault, and wires the agent.
245
245
 
246
+ Every one of these is also a button in the app. Click the Compartment icon
247
+ in your menu bar or notification area, and under **CONNECT AN AGENT** press
248
+ Claude, Hermes or OpenClaw. The button runs the same `compartment integrate`
249
+ command for you, so nobody has to open a terminal a second time.
250
+
246
251
  **Claude (Code + Desktop)** - macOS / Linux:
247
252
  ```bash
248
253
  pip install compartment && compartment init && compartment integrate claude
@@ -197,6 +197,11 @@ network at all.
197
197
  One command per platform. Each installs the package, creates your
198
198
  encrypted vault, and wires the agent.
199
199
 
200
+ Every one of these is also a button in the app. Click the Compartment icon
201
+ in your menu bar or notification area, and under **CONNECT AN AGENT** press
202
+ Claude, Hermes or OpenClaw. The button runs the same `compartment integrate`
203
+ command for you, so nobody has to open a terminal a second time.
204
+
200
205
  **Claude (Code + Desktop)** - macOS / Linux:
201
206
  ```bash
202
207
  pip install compartment && compartment init && compartment integrate claude
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "compartment"
7
- version = "3.3"
7
+ version = "3.4"
8
8
  description = "Compartment - high-security, fully offline, encrypted vector memory for AI agents (MCP)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,6 +1,6 @@
1
1
  """Compartment - high-security, fully offline, encrypted vector memory for AI agents."""
2
2
 
3
- __version__ = "3.3"
3
+ __version__ = "3.4"
4
4
 
5
5
  from . import offline_guard as _og
6
6
 
@@ -279,6 +279,40 @@ def change_passphrase(vault: str, new: str, repeat: str) -> tuple[bool, str]:
279
279
  return False, (out[:120] or "could not change the passphrase")
280
280
 
281
281
 
282
+ #: The agents `compartment integrate` can wire, in the order they are offered.
283
+ #: Kept here rather than in the UI so the menu bar and the tray cannot drift
284
+ #: apart, and so adding a target is one edit.
285
+ INTEGRATION_TARGETS = (("claude", "Claude"),
286
+ ("hermes", "Hermes"),
287
+ ("openclaw", "OpenClaw"))
288
+
289
+
290
+ def integrate(vault: str, target: str) -> tuple[bool, str]:
291
+ """Connect an agent to this vault, from the panel. Returns (ok, message).
292
+
293
+ The same command the README gives, run for you. It is offered here
294
+ because the terminal step after installing is where people stop: the
295
+ vault exists, the icon is in the menu bar, and nothing is using it yet
296
+ because nobody knew there was a second command.
297
+
298
+ Wiring an agent edits that agent's own configuration, so this reports
299
+ what happened rather than assuming - a Claude Code CLI that is not
300
+ installed is a normal outcome, not an error to hide.
301
+ """
302
+ if target not in dict(INTEGRATION_TARGETS):
303
+ return False, f"unknown target {target!r}"
304
+ code, out = _run([*_cli_argv(), "--vault", vault, "integrate", target],
305
+ timeout=300)
306
+ text = " ".join(out.split())
307
+ if code != 0:
308
+ return False, (text[:200] or f"could not connect {target}")
309
+ name = dict(INTEGRATION_TARGETS)[target]
310
+ if "not found" in text.lower():
311
+ return True, (f"{name} is not installed on this machine yet. "
312
+ "Compartment is ready for it when it is.")
313
+ return True, f"{name} is connected. Restart it to pick up the change."
314
+
315
+
282
316
  def summarise(state: dict) -> str:
283
317
  """One line under the title. Also what --self-check prints."""
284
318
  if not state["exists"]:
@@ -592,6 +626,7 @@ def run(vault: str | None = None, show: bool = False,
592
626
  self.pw_new = None
593
627
  self.pw_repeat = None
594
628
  self.change_note = None
629
+ self.connect_note = None # what the last Connect button reported
595
630
  return self
596
631
 
597
632
  # ---- building the popover contents -----------------------------
@@ -737,6 +772,27 @@ def run(vault: str | None = None, show: bool = False,
737
772
  10, secondary=True))
738
773
  views.append(divider())
739
774
 
775
+ # Installing leaves you with a vault and an icon, and nothing
776
+ # using either until an agent is wired to it. That step is one
777
+ # terminal command, which is one command too many for most
778
+ # people, so it is a button.
779
+ views.append(label("CONNECT AN AGENT", 10, bold=True,
780
+ secondary=True))
781
+ connect_buttons = []
782
+ for i, (_target, name) in enumerate(INTEGRATION_TARGETS):
783
+ b = NSButton.buttonWithTitle_target_action_(
784
+ name, self, "connectAgent:")
785
+ b.setTag_(i)
786
+ connect_buttons.append(b)
787
+ views.append(row(*connect_buttons, _spacer()))
788
+ views.append(label(
789
+ self.connect_note or
790
+ "Registers memory with the agent, writes its instructions "
791
+ "and turns on capture. The same as running `compartment "
792
+ "integrate claude` in a terminal.", 10, secondary=True,
793
+ wrap=True))
794
+ views.append(divider())
795
+
740
796
  views.append(label(f"LAST {RECENT_COUNT} MEMORIES", 10, bold=True,
741
797
  secondary=True))
742
798
  if st["locked"]:
@@ -871,6 +927,15 @@ def run(vault: str | None = None, show: bool = False,
871
927
  set_setting(vault_path, "auto_lock_minutes", AUTO_LOCK_CHOICES[idx])
872
928
  self.rebuild()
873
929
 
930
+ def connectAgent_(self, sender):
931
+ try:
932
+ target = INTEGRATION_TARGETS[int(sender.tag())][0]
933
+ except (IndexError, ValueError):
934
+ return
935
+ _ok, msg = integrate(vault_path, target)
936
+ self.connect_note = msg
937
+ self.rebuild()
938
+
874
939
  def refresh_(self, sender):
875
940
  self.rebuild()
876
941
 
@@ -29,10 +29,11 @@ from pathlib import Path
29
29
 
30
30
  from .home import env, home
31
31
 
32
- from .menubar import (AUTO_LOCK_CHOICES, RECENT_COUNT, auto_lock_label,
33
- change_passphrase, claim_first_run, default_vault,
34
- fetch_state, lock_vault, self_check, set_setting,
35
- starter_note, summarise, unlock_vault)
32
+ from .menubar import (AUTO_LOCK_CHOICES, INTEGRATION_TARGETS, RECENT_COUNT,
33
+ auto_lock_label, change_passphrase, claim_first_run,
34
+ default_vault, fetch_state, integrate, lock_vault,
35
+ self_check, set_setting, starter_note, summarise,
36
+ unlock_vault)
36
37
 
37
38
  PANEL_WIDTH = 360
38
39
  PANEL_MAX_HEIGHT = 640
@@ -106,6 +107,16 @@ def panel_rows(state: dict) -> list[tuple[str, str]]:
106
107
  f"{'on' if s['search_starter_facts'] else 'off'}"))
107
108
  rows.append(("choice:auto_lock_minutes",
108
109
  f"Auto-lock: {auto_lock_label(s['auto_lock_minutes'])}"))
110
+ # Installing leaves you with a vault that nothing is using until an agent
111
+ # is wired to it. One button per agent, so that step is not a terminal
112
+ # command someone has to know about.
113
+ rows.append(("heading", "CONNECT AN AGENT"))
114
+ for target, name in INTEGRATION_TARGETS:
115
+ rows.append((f"connect:{target}", name))
116
+ rows.append(("note", state.get("connect_note") or
117
+ "Registers memory with the agent, writes its instructions "
118
+ "and turns on capture. The same as running "
119
+ "`compartment integrate claude` in a terminal."))
109
120
  rows.append(("heading", f"LAST {RECENT_COUNT} MEMORIES"))
110
121
  recent = state.get("recent") or []
111
122
  if not recent:
@@ -335,13 +346,36 @@ def run(vault: str | None = None, show: bool = False,
335
346
  *[auto_lock_label(m) for m in AUTO_LOCK_CHOICES],
336
347
  command=set_lock).pack(side="right")
337
348
 
349
+ ttk.Separator(frame).pack(fill="x", pady=10)
350
+ ttk.Label(frame, text="CONNECT AN AGENT",
351
+ font=("Segoe UI", 8, "bold")).pack(anchor="w")
352
+
353
+ def connect(target):
354
+ _ok, note = integrate(vault_path, target)
355
+ panel["connect_note"] = note
356
+ refresh()
357
+
358
+ connect_row = ttk.Frame(frame)
359
+ connect_row.pack(fill="x", pady=(6, 0))
360
+ for _target, _name in INTEGRATION_TARGETS:
361
+ ttk.Button(connect_row, text=_name,
362
+ command=lambda t=_target: connect(t)
363
+ ).pack(side="left", padx=(0, 6))
364
+ ttk.Label(frame, text=panel.get("connect_note") or
365
+ "Registers memory with the agent, writes its instructions "
366
+ "and turns on capture. The same as running "
367
+ "`compartment integrate claude` in a terminal.",
368
+ foreground="#666", wraplength=WRAP,
369
+ justify="left").pack(anchor="w", pady=(4, 0))
370
+
338
371
  ttk.Separator(frame).pack(fill="x", pady=10)
339
372
  ttk.Label(frame, text=f"LAST {RECENT_COUNT} MEMORIES",
340
373
  font=("Segoe UI", 8, "bold")).pack(anchor="w")
341
374
  recent = state.get("recent") or []
342
375
  if not recent:
343
- ttk.Label(frame, text="Nothing yet.",
344
- foreground="#666").pack(anchor="w", pady=(4, 0))
376
+ ttk.Label(frame, text=starter_note(state), foreground="#666",
377
+ wraplength=WRAP,
378
+ justify="left").pack(anchor="w", pady=(4, 0))
345
379
  for r in recent:
346
380
  ttk.Label(frame, text=(r.get("text") or "").strip(),
347
381
  wraplength=WRAP,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: compartment
3
- Version: 3.3
3
+ Version: 3.4
4
4
  Summary: Compartment - high-security, fully offline, encrypted vector memory for AI agents (MCP)
5
5
  Project-URL: Homepage, https://github.com/MaxFreedomPollard/Compartment
6
6
  Project-URL: Source, https://github.com/MaxFreedomPollard/Compartment
@@ -243,6 +243,11 @@ network at all.
243
243
  One command per platform. Each installs the package, creates your
244
244
  encrypted vault, and wires the agent.
245
245
 
246
+ Every one of these is also a button in the app. Click the Compartment icon
247
+ in your menu bar or notification area, and under **CONNECT AN AGENT** press
248
+ Claude, Hermes or OpenClaw. The button runs the same `compartment integrate`
249
+ command for you, so nobody has to open a terminal a second time.
250
+
246
251
  **Claude (Code + Desktop)** - macOS / Linux:
247
252
  ```bash
248
253
  pip install compartment && compartment init && compartment integrate claude
@@ -46,6 +46,7 @@ tests/test_attempt_pacing.py
46
46
  tests/test_audit_chain.py
47
47
  tests/test_claude_hooks_and_integrate.py
48
48
  tests/test_claude_memory_and_recent.py
49
+ tests/test_connect_buttons.py
49
50
  tests/test_crypto.py
50
51
  tests/test_dash.py
51
52
  tests/test_instructions.py
@@ -0,0 +1,165 @@
1
+ """Connecting an agent is a button, on both platforms.
2
+
3
+ `compartment integrate <agent>` is the step people miss: the vault exists,
4
+ the icon is in the menu bar, and nothing is using either because the second
5
+ command was never run. The panel offers it directly, and these tests hold the
6
+ two front ends to the same list of agents as the CLI.
7
+ """
8
+ import re
9
+
10
+ import pytest
11
+
12
+ from compartment import cli, menubar, systray
13
+
14
+
15
+ def _state(**over):
16
+ st = {"vault": "/v", "exists": True, "locked": False, "records": 6665,
17
+ "organic": 0, "recent": [], "error": None,
18
+ "settings": {"capture_hook": True, "search_starter_facts": True,
19
+ "auto_lock_minutes": 30}}
20
+ st.update(over)
21
+ return st
22
+
23
+
24
+ # ------------------------------------------------------------- the agent list
25
+
26
+ @pytest.mark.parametrize("target,_name", menubar.INTEGRATION_TARGETS)
27
+ def test_every_button_names_a_target_the_cli_accepts(monkeypatch, target,
28
+ _name):
29
+ """A button for an agent the CLI rejects is a button that cannot work.
30
+ Goes through the real parser, which is the only thing that can say."""
31
+ seen = {}
32
+ monkeypatch.setattr(cli, "cmd_integrate",
33
+ lambda args: seen.update(vars(args)))
34
+ cli.main(["integrate", target])
35
+ assert seen["target"] == target
36
+
37
+
38
+ def test_no_agent_the_cli_accepts_is_missing_a_button(monkeypatch, capsys):
39
+ """The other direction: adding a target to the CLI and forgetting the
40
+ button leaves an agent nobody can reach from the app. argparse names
41
+ every choice it accepts when it rejects one, so ask it."""
42
+ monkeypatch.setattr(cli, "cmd_integrate", lambda args: None)
43
+ with pytest.raises(SystemExit):
44
+ cli.main(["integrate", "no-such-agent"])
45
+ err = capsys.readouterr().err
46
+ choices = set(re.findall(r"'([a-z]+)'", err.split("choose from")[-1]))
47
+ assert choices == {t for t, _ in menubar.INTEGRATION_TARGETS}
48
+
49
+
50
+ def test_the_agents_are_offered_in_a_fixed_order():
51
+ assert [t for t, _ in menubar.INTEGRATION_TARGETS] == \
52
+ ["claude", "hermes", "openclaw"]
53
+
54
+
55
+ def test_every_agent_has_a_display_name():
56
+ for target, name in menubar.INTEGRATION_TARGETS:
57
+ assert name and name.lower().replace(" ", "") == target
58
+
59
+
60
+ # ---------------------------------------------------------------- the action
61
+
62
+ def test_an_unknown_target_never_runs_anything(monkeypatch):
63
+ called = []
64
+ monkeypatch.setattr(menubar, "_run", lambda *a, **k: called.append(a))
65
+ ok, msg = menubar.integrate("/v", "notanagent")
66
+ assert ok is False and "notanagent" in msg
67
+ assert called == []
68
+
69
+
70
+ def test_it_runs_the_documented_command(monkeypatch):
71
+ seen = {}
72
+
73
+ def fake_run(args, timeout=60):
74
+ seen["args"] = args
75
+ return 0, "registered."
76
+ monkeypatch.setattr(menubar, "_run", fake_run)
77
+ ok, _ = menubar.integrate("/path/to/v.vault", "claude")
78
+ assert ok is True
79
+ assert seen["args"][-3:] == ["v.vault", "integrate", "claude"] or \
80
+ seen["args"][-2:] == ["integrate", "claude"]
81
+ assert "--vault" in seen["args"]
82
+ assert "/path/to/v.vault" in seen["args"]
83
+
84
+
85
+ def test_success_tells_the_user_to_restart_the_agent(monkeypatch):
86
+ monkeypatch.setattr(menubar, "_run", lambda *a, **k: (0, "registered."))
87
+ ok, msg = menubar.integrate("/v", "claude")
88
+ assert ok is True
89
+ assert "Claude" in msg and "estart" in msg
90
+
91
+
92
+ def test_a_missing_agent_is_reported_as_normal_not_as_failure(monkeypatch):
93
+ """Not having Hermes installed is not an error, and must not read as
94
+ one: the wiring that can be done is still done."""
95
+ monkeypatch.setattr(menubar, "_run",
96
+ lambda *a, **k: (0, "hermes not found; finish with…"))
97
+ ok, msg = menubar.integrate("/v", "hermes")
98
+ assert ok is True
99
+ assert "not installed" in msg
100
+
101
+
102
+ def test_a_real_failure_surfaces_its_reason(monkeypatch):
103
+ monkeypatch.setattr(menubar, "_run",
104
+ lambda *a, **k: (1, "permission denied writing config"))
105
+ ok, msg = menubar.integrate("/v", "claude")
106
+ assert ok is False
107
+ assert "permission denied" in msg
108
+
109
+
110
+ def test_a_failure_with_no_output_still_says_something(monkeypatch):
111
+ monkeypatch.setattr(menubar, "_run", lambda *a, **k: (1, ""))
112
+ ok, msg = menubar.integrate("/v", "openclaw")
113
+ assert ok is False and msg
114
+
115
+
116
+ def test_wiring_gets_a_longer_leash_than_a_status_call(monkeypatch):
117
+ """Installing into the Hermes venv can take a while; a 60 s default would
118
+ time out mid-pip and leave the agent half-wired."""
119
+ seen = {}
120
+
121
+ def fake_run(args, timeout=60):
122
+ seen["timeout"] = timeout
123
+ return 0, "ok"
124
+ monkeypatch.setattr(menubar, "_run", fake_run)
125
+ menubar.integrate("/v", "hermes")
126
+ assert seen["timeout"] >= 300
127
+
128
+
129
+ # ----------------------------------------------------------------- the panel
130
+
131
+ def test_the_tray_panel_has_a_button_per_agent():
132
+ rows = systray.panel_rows(_state())
133
+ connects = [text for kind, text in rows if kind.startswith("connect:")]
134
+ assert connects == ["Claude", "Hermes", "OpenClaw"]
135
+
136
+
137
+ def test_the_tray_panel_heads_the_section():
138
+ rows = systray.panel_rows(_state())
139
+ assert ("heading", "CONNECT AN AGENT") in rows
140
+
141
+
142
+ def test_the_tray_panel_explains_what_the_buttons_do():
143
+ rows = systray.panel_rows(_state())
144
+ note = [t for k, t in rows if k == "note"][0]
145
+ assert "integrate claude" in note
146
+
147
+
148
+ def test_the_tray_panel_shows_the_last_result_once_there_is_one():
149
+ rows = systray.panel_rows(_state(connect_note="Claude is connected."))
150
+ note = [t for k, t in rows if k == "note"][0]
151
+ assert note == "Claude is connected."
152
+
153
+
154
+ def test_the_buttons_are_offered_even_while_the_vault_is_locked():
155
+ """Wiring an agent edits the agent's config, not the vault, so a locked
156
+ vault is no reason to hide it."""
157
+ rows = systray.panel_rows(_state(locked=True))
158
+ assert [t for k, t in rows if k.startswith("connect:")] == \
159
+ ["Claude", "Hermes", "OpenClaw"]
160
+
161
+
162
+ @pytest.mark.parametrize("target,name", menubar.INTEGRATION_TARGETS)
163
+ def test_each_row_carries_the_target_the_action_needs(target, name):
164
+ rows = systray.panel_rows(_state())
165
+ assert (f"connect:{target}", name) in rows
@@ -91,7 +91,8 @@ def test_missing_vault_and_errors_are_shown_not_raised():
91
91
  def test_settings_toggles_are_exactly_the_three_the_mac_panel_has():
92
92
  from compartment import menubar
93
93
  rows = systray.panel_rows(_state())
94
- keys = {k.split(":", 1)[1] for k, _ in rows if ":" in k}
94
+ keys = {k.split(":", 1)[1] for k, _ in rows
95
+ if k.startswith(("toggle:", "choice:"))}
95
96
  assert keys == set(menubar.fetch_state("/does/not/exist")["settings"])
96
97
 
97
98
 
File without changes
File without changes
File without changes