@agentikos/omega-os 0.19.41 → 0.19.43

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 (42) hide show
  1. package/bootstrap/lib/__pycache__/llm-clis.cpython-313.pyc +0 -0
  2. package/bootstrap/lib/common.sh +6 -0
  3. package/bootstrap/lib/llm-clis.py +6 -0
  4. package/bootstrap/lib/manifest-helpers.py +110 -0
  5. package/bootstrap/lib/steps.sh +230 -26
  6. package/bootstrap/templates/aisb/CLAUDE.md +13 -0
  7. package/install.sh +8 -2
  8. package/omega/Agentik_Engine/omega_engine/__init__.py +1 -1
  9. package/omega/Agentik_Engine/omega_engine/__pycache__/__init__.cpython-313.pyc +0 -0
  10. package/omega/Agentik_Engine/omega_engine/__pycache__/cli.cpython-313.pyc +0 -0
  11. package/omega/Agentik_Engine/omega_engine/__pycache__/hermes.cpython-313.pyc +0 -0
  12. package/omega/Agentik_Engine/omega_engine/__pycache__/paperclip_bridge.cpython-313.pyc +0 -0
  13. package/omega/Agentik_Engine/omega_engine/__pycache__/personas.cpython-313.pyc +0 -0
  14. package/omega/Agentik_Engine/omega_engine/__pycache__/provider.cpython-313.pyc +0 -0
  15. package/omega/Agentik_Engine/omega_engine/__pycache__/tmux.cpython-313.pyc +0 -0
  16. package/omega/Agentik_Engine/omega_engine/__pycache__/tui.cpython-313.pyc +0 -0
  17. package/omega/Agentik_Engine/omega_engine/cli.py +44 -7
  18. package/omega/Agentik_Engine/omega_engine/hermes.py +43 -1
  19. package/omega/Agentik_Engine/omega_engine/paperclip_bridge.py +22 -0
  20. package/omega/Agentik_Engine/omega_engine/personas.py +11 -3
  21. package/omega/Agentik_Engine/omega_engine/provider.py +18 -3
  22. package/omega/Agentik_Engine/omega_engine/tmux.py +192 -42
  23. package/omega/Agentik_Engine/omega_engine/tui.py +8 -7
  24. package/omega/Agentik_Engine/pyproject.toml +1 -1
  25. package/omega/Agentik_Engine/tests/__pycache__/test_install_steps_v0_19_43.cpython-313-pytest-8.4.2.pyc +0 -0
  26. package/omega/Agentik_Engine/tests/__pycache__/test_install_steps_v0_19_43.cpython-313.pyc +0 -0
  27. package/omega/Agentik_Engine/tests/__pycache__/test_installer_wiring.cpython-313-pytest-8.4.2.pyc +0 -0
  28. package/omega/Agentik_Engine/tests/__pycache__/test_installer_wiring.cpython-313.pyc +0 -0
  29. package/omega/Agentik_Engine/tests/__pycache__/test_tmux_palette.cpython-313-pytest-8.4.2.pyc +0 -0
  30. package/omega/Agentik_Engine/tests/__pycache__/test_tmux_palette.cpython-313.pyc +0 -0
  31. package/omega/Agentik_Engine/tests/__pycache__/test_v19_43_fixes.cpython-313-pytest-8.4.2.pyc +0 -0
  32. package/omega/Agentik_Engine/tests/__pycache__/test_v19_43_fixes.cpython-313.pyc +0 -0
  33. package/omega/Agentik_Engine/tests/test_install_steps_v0_19_43.py +242 -0
  34. package/omega/Agentik_Engine/tests/test_installer_wiring.py +128 -1
  35. package/omega/Agentik_Engine/tests/test_tmux_palette.py +158 -0
  36. package/omega/Agentik_Engine/tests/test_v19_43_fixes.py +265 -0
  37. package/omega/Agentik_SSOT/VERSION +1 -1
  38. package/omega/Agentik_SSOT/docs/AUDIT-V0.19.43.md +92 -0
  39. package/omega/Agentik_SSOT/rules/audit-gates.md +2 -2
  40. package/omega/Agentik_SSOT/rules/constitution.md +18 -0
  41. package/omega/Agentik_SSOT/rules/three-laws.md +2 -0
  42. package/package.json +1 -1
@@ -0,0 +1,242 @@
1
+ """v0.19.43 install-step gap tests.
2
+
3
+ Three system-dep regressions had bitten fresh-VPS installs:
4
+
5
+ 1. Node.js + npm were never installed by step_system_deps, yet 7 of 13
6
+ supported LLM CLIs (claude_code, gemini_cli, codex, opencode,
7
+ qwen_code, continue_dev, gh_copilot) install via `npm -g`.
8
+
9
+ 2. detect_os only matched apt-get and dnf on Linux — Arch (pacman),
10
+ Alpine (apk), and openSUSE/SLES (zypper) hosts hit
11
+ "no supported package manager" and aborted.
12
+
13
+ 3. step_engine called `uv venv` with no `--python` pin, so on macOS
14
+ Tahoe the broken brew Python 3.14 (libexpat ABI mismatch) was
15
+ silently inherited and the engine venv failed at first import.
16
+
17
+ These tests grep the install scripts directly (no execve of the
18
+ installer) so they run on any host, run fast, and never touch real
19
+ package state.
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import re
24
+ import unittest
25
+ from pathlib import Path
26
+
27
+ REPO_ROOT = Path(__file__).resolve().parents[3]
28
+ COMMON_SH = REPO_ROOT / "bootstrap" / "lib" / "common.sh"
29
+ STEPS_SH = REPO_ROOT / "bootstrap" / "lib" / "steps.sh"
30
+
31
+
32
+ class TestNodeNpmInPkgLists(unittest.TestCase):
33
+ """FIX 1 — every pkg-manager branch in step_system_deps must install
34
+ nodejs + npm so the npm-backed LLM CLIs work after a fresh install."""
35
+
36
+ def setUp(self) -> None:
37
+ self.text = STEPS_SH.read_text()
38
+ # Isolate the body of step_system_deps so we don't accidentally
39
+ # match nodejs/npm in some other helper (e.g. step_clis comments).
40
+ m = re.search(
41
+ r"step_system_deps\(\)\s*\{(.+?)^\}\s*$",
42
+ self.text, re.DOTALL | re.MULTILINE,
43
+ )
44
+ self.assertIsNotNone(m, "step_system_deps body not found in steps.sh")
45
+ self.body = m.group(1)
46
+ # The main package install case statement is the FIRST case block
47
+ # inside step_system_deps. The age install + tmux version check
48
+ # come AFTER it. We pin our assertions to the first case.
49
+ case_match = re.search(
50
+ r"case\s+\"\$OMEGA_PKG\"\s+in\s*\n(.*?)\s+esac",
51
+ self.body, re.DOTALL,
52
+ )
53
+ self.assertIsNotNone(case_match, "OMEGA_PKG case block not found")
54
+ self.case_block = case_match.group(1)
55
+
56
+ @staticmethod
57
+ def _branch_body(case_block: str, name: str) -> str | None:
58
+ """Return the full body (possibly multi-line via `\\` continuation)
59
+ of a `name)` arm in a case statement, ending at `;;`."""
60
+ m = re.search(
61
+ rf"^\s*{name}\)(.*?);;", case_block,
62
+ re.MULTILINE | re.DOTALL,
63
+ )
64
+ return m.group(0) if m else None
65
+
66
+ def test_apt_installs_nodejs_npm(self):
67
+ line = self._branch_body(self.case_block, "apt")
68
+ self.assertIsNotNone(line, "apt branch missing")
69
+ self.assertIn("nodejs", line, f"apt: nodejs missing\n{line}")
70
+ self.assertIn("npm", line, f"apt: npm missing\n{line}")
71
+
72
+ def test_dnf_installs_nodejs_npm(self):
73
+ line = self._branch_body(self.case_block, "dnf")
74
+ self.assertIsNotNone(line, "dnf branch missing")
75
+ self.assertIn("nodejs", line)
76
+ self.assertIn("npm", line)
77
+
78
+ def test_brew_installs_node(self):
79
+ # On macOS Homebrew, `node` bundles npm — no separate `npm`
80
+ # formula. Asserting on the formula name keeps the test honest.
81
+ line = self._branch_body(self.case_block, "brew")
82
+ self.assertIsNotNone(line, "brew branch missing")
83
+ self.assertIn("node", line)
84
+
85
+ def test_pacman_installs_nodejs_npm(self):
86
+ line = self._branch_body(self.case_block, "pacman")
87
+ self.assertIsNotNone(line, "pacman branch missing — Arch/Manjaro not supported")
88
+ self.assertIn("nodejs", line)
89
+ self.assertIn("npm", line)
90
+
91
+ def test_apk_installs_nodejs_npm(self):
92
+ line = self._branch_body(self.case_block, "apk")
93
+ self.assertIsNotNone(line, "apk branch missing — Alpine not supported")
94
+ self.assertIn("nodejs", line)
95
+ self.assertIn("npm", line)
96
+
97
+ def test_zypper_installs_nodejs_npm(self):
98
+ line = self._branch_body(self.case_block, "zypper")
99
+ self.assertIsNotNone(line, "zypper branch missing — openSUSE/SLES not supported")
100
+ self.assertIn("nodejs", line)
101
+ self.assertIn("npm", line)
102
+
103
+
104
+ class TestDetectOsSupports5Managers(unittest.TestCase):
105
+ """FIX 2 — common.sh::detect_os must probe all 5 Linux pkg managers
106
+ + Darwin/brew before deciding 'unknown'."""
107
+
108
+ def test_detect_os_checks_5_pkg_managers(self):
109
+ text = COMMON_SH.read_text()
110
+ # Isolate the detect_os body.
111
+ m = re.search(
112
+ r"detect_os\(\)\s*\{(.+?)^\}\s*$",
113
+ text, re.DOTALL | re.MULTILINE,
114
+ )
115
+ self.assertIsNotNone(m, "detect_os body not found in common.sh")
116
+ body = m.group(1)
117
+ for cmd in ("apt-get", "dnf", "pacman", "apk", "zypper"):
118
+ self.assertIn(
119
+ f"have {cmd}", body,
120
+ f"detect_os does not probe '{cmd}' — distro will hit unknown",
121
+ )
122
+ # macOS still resolves to brew.
123
+ self.assertIn('OMEGA_PKG="brew"', body)
124
+
125
+
126
+ class TestStepEnginePinsPython313(unittest.TestCase):
127
+ """FIX 3 — step_engine must request Python 3.13 from uv with a
128
+ graceful fallback so macOS Tahoe's broken brew Python 3.14 (libexpat
129
+ ABI mismatch) cannot silently break the engine venv."""
130
+
131
+ def test_step_engine_pins_python_3_13(self):
132
+ text = STEPS_SH.read_text()
133
+ m = re.search(
134
+ r"step_engine\(\)\s*\{(.+?)^\}\s*$",
135
+ text, re.DOTALL | re.MULTILINE,
136
+ )
137
+ self.assertIsNotNone(m, "step_engine body not found")
138
+ body = m.group(1)
139
+ self.assertIn(
140
+ "--python 3.13", body,
141
+ "step_engine no longer pins uv venv to Python 3.13 — "
142
+ "macOS Tahoe brew Python 3.14 will leak in again",
143
+ )
144
+ # And there must be a fallback to a bare `uv venv` so the install
145
+ # still completes when Python 3.13 cannot be procured.
146
+ self.assertIn("uv_bin", body)
147
+ self.assertRegex(
148
+ body, r'"\$uv_bin"\s+venv\s+(?:>/dev/null|--python|\s*\|\|)',
149
+ "expected at least one fallback `uv venv` call after the pinned attempt",
150
+ )
151
+
152
+
153
+ class TestTmuxVersionCheckPresent(unittest.TestCase):
154
+ """FIX 4 — step_system_deps must warn (non-fatal) when tmux < 3.3,
155
+ because the bundled pro config uses 3.3+ syntax."""
156
+
157
+ def test_tmux_version_check_present(self):
158
+ text = STEPS_SH.read_text()
159
+ m = re.search(
160
+ r"step_system_deps\(\)\s*\{(.+?)^\}\s*$",
161
+ text, re.DOTALL | re.MULTILINE,
162
+ )
163
+ self.assertIsNotNone(m, "step_system_deps body not found")
164
+ body = m.group(1)
165
+ self.assertIn("tmux -V", body, "no tmux version detection")
166
+ # The warning must mention 3.3 explicitly so an operator can
167
+ # grep the install log and find it.
168
+ self.assertIn("3.3", body, "no mention of the 3.3 minimum in the warning")
169
+ # And it must use info/warn (non-fatal) — never `die`/`return 1`.
170
+ # We assert the check sits AFTER the package install and DOES
171
+ # NOT return non-zero.
172
+ tmux_block = re.search(
173
+ r"tmux -V.+?fi\s*\n\s*fi", body, re.DOTALL,
174
+ )
175
+ self.assertIsNotNone(tmux_block, "tmux version check block not delimited")
176
+ self.assertNotIn("return 1", tmux_block.group(0),
177
+ "tmux version check must be non-fatal")
178
+
179
+
180
+ class TestPkgNameTranslationForPacmanApkZypper(unittest.TestCase):
181
+ """The 3 newly-supported managers use different package names for
182
+ whiptail (libnewt/newt) and python3-yaml (python-yaml/py3-yaml/
183
+ python3-PyYAML). This test pins those translations so a future edit
184
+ cannot silently drop them."""
185
+
186
+ def setUp(self) -> None:
187
+ text = STEPS_SH.read_text()
188
+ m = re.search(
189
+ r"step_system_deps\(\)\s*\{(.+?)^\}\s*$",
190
+ text, re.DOTALL | re.MULTILINE,
191
+ )
192
+ self.assertIsNotNone(m)
193
+ body = m.group(1)
194
+ case_match = re.search(
195
+ r"case\s+\"\$OMEGA_PKG\"\s+in\s*\n(.*?)\s+esac",
196
+ body, re.DOTALL,
197
+ )
198
+ self.assertIsNotNone(case_match)
199
+ self.case_block = case_match.group(1)
200
+
201
+ def _branch(self, name: str) -> str:
202
+ m = re.search(
203
+ rf"^\s*{name}\)(.*?);;", self.case_block,
204
+ re.MULTILINE | re.DOTALL,
205
+ )
206
+ self.assertIsNotNone(m, f"{name} branch not found in case block")
207
+ return m.group(0)
208
+
209
+ def test_pacman_uses_libnewt_and_python_yaml(self):
210
+ line = self._branch("pacman")
211
+ # Arch packages whiptail's library as `libnewt`, not `whiptail`
212
+ # or `newt` — passing `whiptail` would 404.
213
+ self.assertIn("libnewt", line, "pacman: must use libnewt, not whiptail/newt")
214
+ # Arch ships PyYAML as `python-yaml`.
215
+ self.assertIn("python-yaml", line, "pacman: must use python-yaml")
216
+ # And it must NOT carry the apt/dnf name through.
217
+ self.assertNotIn("whiptail", line)
218
+ self.assertNotIn("python3-yaml", line)
219
+ self.assertNotIn("python3-pyyaml", line)
220
+
221
+ def test_apk_uses_newt_and_py3_yaml(self):
222
+ line = self._branch("apk")
223
+ # Alpine packages whiptail as `newt`.
224
+ self.assertIn("newt", line, "apk: must use newt")
225
+ # Alpine ships PyYAML as `py3-yaml`.
226
+ self.assertIn("py3-yaml", line, "apk: must use py3-yaml")
227
+ self.assertNotIn("whiptail", line)
228
+ self.assertNotIn("python3-yaml", line)
229
+ self.assertNotIn("python3-pyyaml", line)
230
+
231
+ def test_zypper_uses_newt_and_python3_PyYAML(self):
232
+ line = self._branch("zypper")
233
+ # openSUSE packages whiptail's library as `newt`.
234
+ self.assertIn("newt", line, "zypper: must use newt")
235
+ # openSUSE ships PyYAML with the canonical PyYAML capitalisation.
236
+ self.assertIn("python3-PyYAML", line, "zypper: must use python3-PyYAML")
237
+ self.assertNotIn("whiptail", line)
238
+ self.assertNotIn("python-yaml ", line) # trailing space avoids matching python3-PyYAML
239
+
240
+
241
+ if __name__ == "__main__":
242
+ unittest.main(verbosity=2)
@@ -49,7 +49,15 @@ class TestInstallerStepList(unittest.TestCase):
49
49
  ("36-tmux-config", "step_tmux_config"),
50
50
  ("37-hermes-brief", "step_hermes_brief"),
51
51
  ("38-personas", "step_personas"),
52
- ("59-hermes-session", "step_hermes_session"),
52
+ ("47-paperclip", "step_paperclip"),
53
+ # v0.19.43 — split the misnamed 59-hermes-session into two
54
+ # distinct steps. 59-aisb-session spawns the always-on AISB
55
+ # master chat window (was the actual behaviour of the old
56
+ # 59-hermes-session, despite the misleading name);
57
+ # 60-hermes-session is NEW and conditionally spawns the Hermès
58
+ # chat when the vault key is present.
59
+ ("59-aisb-session", "step_aisb_session"),
60
+ ("60-hermes-session", "step_hermes_session"),
53
61
  ]
54
62
 
55
63
  def setUp(self):
@@ -86,6 +94,48 @@ class TestInstallerStepList(unittest.TestCase):
86
94
  "step_engine must NOT call write_default_config directly anymore "
87
95
  "— that's step_tmux_config's job")
88
96
 
97
+ def test_step_tmux_config_wires_omega_addon_after_upstream(self):
98
+ """v0.19.43 — step_tmux_config MUST call install_into_home_tmux_conf
99
+ AFTER the upstream tmux-claude installer succeeds (or detects an
100
+ existing install). Otherwise the user's ~/.tmux.conf is tmux-claude's
101
+ with NO `source-file` line for the OmegaOS add-on, so the Alt+O bind
102
+ is never sourced. The fallback path already wires it; this guards the
103
+ happy path."""
104
+ m = re.search(r"^step_tmux_config\(\) \{(.+?)^\}", self.steps_text,
105
+ re.DOTALL | re.MULTILINE)
106
+ self.assertIsNotNone(m, "step_tmux_config function not found")
107
+ body = m.group(1)
108
+ self.assertIn("install_into_home_tmux_conf", body,
109
+ "step_tmux_config must call install_into_home_tmux_conf to wire "
110
+ "the Omega add-on (Alt+O bind) into ~/.tmux.conf even when the "
111
+ "upstream tmux-claude installer succeeded on the happy path")
112
+
113
+ def test_step_aisb_session_and_step_hermes_session_distinct(self):
114
+ """v0.19.43 — the misnamed step_hermes_session (which actually
115
+ spawned AISB) was split into step_aisb_session + a new real
116
+ step_hermes_session. Both must exist; step_aisb_session must call
117
+ spawn_aisb_chat; step_hermes_session must call spawn_hermes_chat."""
118
+ # step_aisb_session body must call spawn_aisb_chat
119
+ m_aisb = re.search(r"^step_aisb_session\(\) \{(.+?)^\}",
120
+ self.steps_text, re.DOTALL | re.MULTILINE)
121
+ self.assertIsNotNone(m_aisb,
122
+ "step_aisb_session function not found (rename from "
123
+ "step_hermes_session in v0.19.43)")
124
+ self.assertIn("spawn_aisb_chat", m_aisb.group(1),
125
+ "step_aisb_session must call spawn_aisb_chat")
126
+ # step_hermes_session body must call spawn_hermes_chat AND gate
127
+ # on the vault key (no fallback to Max OAuth for the 24/7 chat).
128
+ m_h = re.search(r"^step_hermes_session\(\) \{(.+?)^\}",
129
+ self.steps_text, re.DOTALL | re.MULTILINE)
130
+ self.assertIsNotNone(m_h,
131
+ "step_hermes_session function not found (new in v0.19.43)")
132
+ h_body = m_h.group(1)
133
+ self.assertIn("spawn_hermes_chat", h_body,
134
+ "step_hermes_session must call spawn_hermes_chat")
135
+ self.assertIn("ANTHROPIC_API_KEY_HERMES", h_body,
136
+ "step_hermes_session must gate the spawn on the vault key — "
137
+ "no silent fallback to Max OAuth for the 24/7 chat")
138
+
89
139
 
90
140
  # ---------------------------------------------------------------------------
91
141
  # Step semantics — exercise the same Python the heredocs run, against a
@@ -627,5 +677,82 @@ class TestDashDashSeparatorAccepted(unittest.TestCase):
627
677
  "not found")
628
678
 
629
679
 
680
+ class TestStepPaperclipBehavior(unittest.TestCase):
681
+ """v0.19.43 — step_paperclip auto-registers OmegaOS as the 'omegaos'
682
+ company with Paperclip at install time. Without this, the user must
683
+ run `omega paperclip register` by hand or Paperclip never sees OmegaOS."""
684
+
685
+ def test_paperclip_registers_14_agents_on_install(self):
686
+ with tempfile.TemporaryDirectory() as tmp:
687
+ home = Path(tmp) / "Omega"
688
+ home.mkdir()
689
+ pc_home = Path(tmp) / "paperclip"
690
+ # Deploy engine code.
691
+ engine_src = REPO_ROOT / "omega" / "Agentik_Engine"
692
+ engine_dst = home / "Agentik_Engine"
693
+ engine_dst.mkdir()
694
+ import shutil
695
+ for entry in engine_src.iterdir():
696
+ if entry.name in {"tests", "__pycache__"}:
697
+ continue
698
+ if entry.is_dir():
699
+ shutil.copytree(entry, engine_dst / entry.name)
700
+ else:
701
+ (engine_dst / entry.name).write_bytes(entry.read_bytes())
702
+
703
+ old_home = os.environ.get("OMEGA_HOME")
704
+ old_pc = os.environ.get("PAPERCLIP_HOME")
705
+ os.environ["OMEGA_HOME"] = str(home)
706
+ os.environ["PAPERCLIP_HOME"] = str(pc_home)
707
+ sys.path.insert(0, str(engine_dst))
708
+ try:
709
+ import importlib
710
+ import omega_engine.paperclip_bridge as PB
711
+ importlib.reload(PB)
712
+ result = PB.register()
713
+ self.assertEqual(result.get("agents_written"), 14,
714
+ f"register() must write 14 agents (Hermès + 13 AISB); "
715
+ f"got {result.get('agents_written')}")
716
+ # Verify files on disk.
717
+ company_dir = pc_home / "companies" / "omegaos"
718
+ self.assertTrue(company_dir.exists(),
719
+ f"company dir must land at {company_dir}")
720
+ self.assertTrue((company_dir / "company.json").exists())
721
+ agents_dir = company_dir / "agents"
722
+ self.assertTrue(agents_dir.exists())
723
+ json_files = list(agents_dir.glob("*.json"))
724
+ self.assertEqual(len(json_files), 14,
725
+ f"expected 14 agent JSON files, found {len(json_files)}")
726
+ finally:
727
+ if old_home is None:
728
+ os.environ.pop("OMEGA_HOME", None)
729
+ else:
730
+ os.environ["OMEGA_HOME"] = old_home
731
+ if old_pc is None:
732
+ os.environ.pop("PAPERCLIP_HOME", None)
733
+ else:
734
+ os.environ["PAPERCLIP_HOME"] = old_pc
735
+ sys.path.remove(str(engine_dst))
736
+
737
+ def test_step_paperclip_heredoc_calls_register(self):
738
+ """Lock in that the heredoc body calls paperclip_bridge.register()."""
739
+ body = STEPS_SH.read_text()
740
+ m = re.search(r"^step_paperclip\(\) \{(.+?)^\}", body,
741
+ re.DOTALL | re.MULTILINE)
742
+ self.assertIsNotNone(m, "step_paperclip function not found in steps.sh")
743
+ fn_body = m.group(1)
744
+ self.assertIn("paperclip_bridge", fn_body,
745
+ "step_paperclip must import paperclip_bridge module")
746
+ self.assertIn("register()", fn_body,
747
+ "step_paperclip must call PB.register()")
748
+
749
+ def test_heartbeat_on_spawn_is_silent_on_failure(self):
750
+ """Even if PAPERCLIP_HOME is unwritable, heartbeat_on_spawn must
751
+ never raise — it's optional infrastructure."""
752
+ from omega_engine.paperclip_bridge import heartbeat_on_spawn
753
+ # Should not raise.
754
+ heartbeat_on_spawn("nobody@test", project="nothing")
755
+
756
+
630
757
  if __name__ == "__main__":
631
758
  unittest.main()
@@ -90,5 +90,163 @@ class TestTmuxClaudePalette(unittest.TestCase):
90
90
  )
91
91
 
92
92
 
93
+ class TestTmuxBindsFixedAndPathsAbsolute(unittest.TestCase):
94
+ """v0.19.42 — regression locks for the broken M-z bind that left the
95
+ user staring at a black screen ("ça lance rien du tout")."""
96
+
97
+ def test_m_z_uses_display_popup_not_run_shell_bg(self):
98
+ from omega_engine.tmux import _PRO_CONFIG, _DEFAULT_CONFIG
99
+ for name, cfg in (("_PRO_CONFIG", _PRO_CONFIG),
100
+ ("_DEFAULT_CONFIG", _DEFAULT_CONFIG)):
101
+ m_z_lines = [ln for ln in cfg.splitlines()
102
+ if ln.startswith("bind-key -n M-z")]
103
+ self.assertTrue(m_z_lines,
104
+ f"{name} must have bind-key -n M-z")
105
+ for ln in m_z_lines:
106
+ self.assertIn("display-popup", ln,
107
+ f"{name} M-z must use display-popup. Got: {ln}")
108
+ self.assertNotIn("run-shell -b", ln,
109
+ f"{name} M-z must NOT use `run-shell -b` (was the "
110
+ f"v0.19.40 bug — background switch-client breaks). "
111
+ f"Got: {ln}")
112
+
113
+ def test_omega_bin_placeholder_in_both_configs(self):
114
+ from omega_engine.tmux import _PRO_CONFIG, _DEFAULT_CONFIG
115
+ for name, cfg in (("_PRO_CONFIG", _PRO_CONFIG),
116
+ ("_DEFAULT_CONFIG", _DEFAULT_CONFIG)):
117
+ self.assertIn("__OMEGA_BIN__", cfg,
118
+ f"{name} must use __OMEGA_BIN__ placeholder for the "
119
+ f"absolute omega path substitution at write-time")
120
+
121
+ def test_write_default_config_substitutes_omega_bin(self):
122
+ import tempfile
123
+ from pathlib import Path
124
+ from omega_engine.tmux import write_default_config
125
+ with tempfile.TemporaryDirectory() as tmp:
126
+ home = Path(tmp) / "Omega"
127
+ home.mkdir()
128
+ written = write_default_config(profile="pro", omega_home=home)
129
+ content = written.read_text()
130
+ self.assertNotIn("__OMEGA_BIN__", content,
131
+ "placeholder must be substituted away")
132
+ self.assertIn(str(home / "Agentik_Tools" / "bin" / "omega"),
133
+ content,
134
+ "the absolute omega-bin path must appear in the conf")
135
+
136
+ def test_install_preserves_tmux_claude(self):
137
+ import tempfile
138
+ from pathlib import Path
139
+ from unittest import mock
140
+ from omega_engine.tmux import install_into_home_tmux_conf
141
+ with tempfile.TemporaryDirectory() as tmp:
142
+ tmp = Path(tmp)
143
+ fake_home = tmp / "fake-home"; fake_home.mkdir()
144
+ omega_home = tmp / "Omega"; omega_home.mkdir()
145
+ home_conf = fake_home / ".tmux.conf"
146
+ home_conf.write_text(
147
+ "# tmux-claude config\nset -g mouse on\n"
148
+ "bind -n M-z display-popup -E "
149
+ "/home/user/.tmux/scripts/session-manager-v2.sh\n"
150
+ )
151
+ with mock.patch.object(Path, "home", lambda: fake_home):
152
+ result = install_into_home_tmux_conf(
153
+ profile="pro", omega_home=omega_home,
154
+ )
155
+ self.assertEqual(result["mode"], "preserved-tmux-claude",
156
+ "install must detect tmux-claude and not overwrite")
157
+ content = home_conf.read_text()
158
+ self.assertIn("session-manager-v2.sh", content,
159
+ "tmux-claude bind must survive — we did NOT clobber")
160
+ self.assertIn("omega-tmux-add.conf", content,
161
+ "install must add a `source-file` line for OmegaOS add-on")
162
+ addon = omega_home / "Agentik_Tools" / "omega-tmux-add.conf"
163
+ self.assertTrue(addon.exists(),
164
+ "add-on conf must be written at "
165
+ "$OMEGA_HOME/Agentik_Tools/omega-tmux-add.conf")
166
+ addon_content = addon.read_text()
167
+ self.assertIn("M-o", addon_content,
168
+ "Omega add-on must bind M-o (Alt+O — a key tmux-claude "
169
+ "leaves free)")
170
+ # Must NOT rebind M-z (reserved by tmux-claude). The string
171
+ # 'M-z' may appear in COMMENTS, so check there's no actual
172
+ # bind-key directive on M-z.
173
+ bind_m_z_lines = [ln for ln in addon_content.splitlines()
174
+ if ln.lstrip().startswith("bind-key")
175
+ and "M-z" in ln]
176
+ self.assertEqual(bind_m_z_lines, [],
177
+ f"Omega add-on must NOT rebind M-z — found: {bind_m_z_lines}")
178
+
179
+ def test_force_overwrites_tmux_claude(self):
180
+ import tempfile
181
+ from pathlib import Path
182
+ from unittest import mock
183
+ from omega_engine.tmux import install_into_home_tmux_conf
184
+ with tempfile.TemporaryDirectory() as tmp:
185
+ tmp = Path(tmp)
186
+ fake_home = tmp / "fake-home"; fake_home.mkdir()
187
+ omega_home = tmp / "Omega"; omega_home.mkdir()
188
+ home_conf = fake_home / ".tmux.conf"
189
+ home_conf.write_text("# tmux-claude\nsession-manager-v2.sh\n")
190
+ with mock.patch.object(Path, "home", lambda: fake_home):
191
+ result = install_into_home_tmux_conf(
192
+ profile="pro", omega_home=omega_home, force=True,
193
+ )
194
+ self.assertEqual(result["mode"], "fresh",
195
+ "force=True must do a fresh overwrite")
196
+ self.assertNotIn("session-manager-v2.sh", home_conf.read_text(),
197
+ "force=True must clobber tmux-claude")
198
+
199
+ # v0.19.43 — spawn_aisb_chat + spawn_hermes_chat must use the
200
+ # WINDOW-under-Omega pattern (spawn_chat_in_omega), not the standalone
201
+ # session pattern (_spawn_with_shell_then_run). This pins the v0.19.31+
202
+ # design promise: everything under Omega.
203
+
204
+ def test_spawn_aisb_chat_uses_window_pattern(self):
205
+ """spawn_aisb_chat must use spawn_chat_in_omega (window under Omega),
206
+ not _spawn_with_shell_then_run (standalone session)."""
207
+ import inspect
208
+ from omega_engine.tmux import spawn_aisb_chat
209
+ src = inspect.getsource(spawn_aisb_chat)
210
+ self.assertIn("spawn_chat_in_omega", src,
211
+ "spawn_aisb_chat must use the window-based pattern, not a "
212
+ "standalone session (the v0.19.31+ design promise)")
213
+ self.assertNotIn("_spawn_with_shell_then_run", src,
214
+ "spawn_aisb_chat must NOT use the standalone-session helper "
215
+ "anymore — refactor to spawn_chat_in_omega")
216
+
217
+ def test_spawn_hermes_chat_uses_window_pattern(self):
218
+ import inspect
219
+ from omega_engine.tmux import spawn_hermes_chat
220
+ src = inspect.getsource(spawn_hermes_chat)
221
+ self.assertIn("spawn_chat_in_omega", src,
222
+ "spawn_hermes_chat must use the window-based pattern")
223
+ self.assertNotIn("_spawn_with_shell_then_run", src,
224
+ "spawn_hermes_chat must NOT use the standalone-session helper "
225
+ "anymore — refactor to spawn_chat_in_omega")
226
+ # Credential isolation MUST be preserved — Hermès still reads the
227
+ # vault key directly so its budget stays separate from Max OAuth.
228
+ self.assertIn("ANTHROPIC_API_KEY_HERMES", src,
229
+ "spawn_hermes_chat must still read the vault key — isolation "
230
+ "from Max OAuth is mandatory (see omega_engine/hermes.py docs)")
231
+
232
+ def test_spawn_aisb_chat_returns_omega_window_ref(self):
233
+ """The return value contract changed from 'AISB-chat' (standalone
234
+ session) to 'Omega:aisb' (window under Omega). Callers in cli.py
235
+ + tui.py rely on this for tmux switch-client targets."""
236
+ import inspect
237
+ from omega_engine.tmux import spawn_aisb_chat
238
+ src = inspect.getsource(spawn_aisb_chat)
239
+ self.assertIn('"Omega:aisb"', src,
240
+ "spawn_aisb_chat must return 'Omega:aisb' so callers can pass "
241
+ "it directly to `tmux switch-client -t`")
242
+
243
+ def test_spawn_hermes_chat_returns_omega_window_ref(self):
244
+ import inspect
245
+ from omega_engine.tmux import spawn_hermes_chat
246
+ src = inspect.getsource(spawn_hermes_chat)
247
+ self.assertIn('"Omega:hermes"', src,
248
+ "spawn_hermes_chat must return 'Omega:hermes'")
249
+
250
+
93
251
  if __name__ == "__main__":
94
252
  unittest.main(verbosity=2)