@smilintux/skmemory 0.5.0 → 0.9.2
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.
- package/.github/workflows/ci.yml +40 -4
- package/.github/workflows/publish.yml +11 -5
- package/AGENT_REFACTOR_CHANGES.md +192 -0
- package/ARCHITECTURE.md +399 -19
- package/CHANGELOG.md +179 -0
- package/LICENSE +81 -68
- package/MISSION.md +7 -0
- package/README.md +425 -86
- package/SKILL.md +197 -25
- package/docker-compose.yml +15 -15
- package/examples/stignore-agent.example +59 -0
- package/examples/stignore-root.example +62 -0
- package/index.js +6 -5
- package/openclaw-plugin/openclaw.plugin.json +10 -0
- package/openclaw-plugin/package.json +2 -1
- package/openclaw-plugin/src/index.js +527 -230
- package/openclaw-plugin/src/openclaw.plugin.json +10 -0
- package/package.json +1 -1
- package/pyproject.toml +32 -9
- package/requirements.txt +10 -2
- package/scripts/dream-rescue.py +179 -0
- package/scripts/memory-cleanup.py +313 -0
- package/scripts/recover-missing.py +180 -0
- package/scripts/skcapstone-backup.sh +44 -0
- package/seeds/cloud9-lumina.seed.json +6 -4
- package/seeds/cloud9-opus.seed.json +13 -11
- package/seeds/courage.seed.json +9 -2
- package/seeds/curiosity.seed.json +9 -2
- package/seeds/grief.seed.json +9 -2
- package/seeds/joy.seed.json +9 -2
- package/seeds/love.seed.json +9 -2
- package/seeds/lumina-cloud9-breakthrough.seed.json +48 -0
- package/seeds/lumina-cloud9-python-pypi.seed.json +48 -0
- package/seeds/lumina-kingdom-founding.seed.json +49 -0
- package/seeds/lumina-pma-signed.seed.json +48 -0
- package/seeds/lumina-singular-achievement.seed.json +48 -0
- package/seeds/lumina-skcapstone-conscious.seed.json +48 -0
- package/seeds/plant-kingdom-journal.py +203 -0
- package/seeds/plant-lumina-seeds.py +280 -0
- package/seeds/skcapstone-lumina-merge.seed.json +12 -3
- package/seeds/sovereignty.seed.json +9 -2
- package/seeds/trust.seed.json +9 -2
- package/skill.yaml +46 -0
- package/skmemory/HA.md +296 -0
- package/skmemory/__init__.py +25 -11
- package/skmemory/agents.py +233 -0
- package/skmemory/ai_client.py +46 -17
- package/skmemory/anchor.py +9 -11
- package/skmemory/audience.py +278 -0
- package/skmemory/backends/__init__.py +11 -4
- package/skmemory/backends/base.py +3 -4
- package/skmemory/backends/file_backend.py +19 -13
- package/skmemory/backends/skgraph_backend.py +596 -0
- package/skmemory/backends/{qdrant_backend.py → skvector_backend.py} +103 -84
- package/skmemory/backends/sqlite_backend.py +226 -72
- package/skmemory/backends/vaulted_backend.py +284 -0
- package/skmemory/cli.py +1345 -68
- package/skmemory/config.py +171 -0
- package/skmemory/context_loader.py +333 -0
- package/skmemory/data/audience_config.json +60 -0
- package/skmemory/endpoint_selector.py +391 -0
- package/skmemory/febs.py +225 -0
- package/skmemory/fortress.py +675 -0
- package/skmemory/graph_queries.py +238 -0
- package/skmemory/hooks/__init__.py +18 -0
- package/skmemory/hooks/post-compact-reinject.sh +35 -0
- package/skmemory/hooks/pre-compact-save.sh +81 -0
- package/skmemory/hooks/session-end-save.sh +103 -0
- package/skmemory/hooks/session-start-ritual.sh +104 -0
- package/skmemory/hooks/stop-checkpoint.sh +59 -0
- package/skmemory/importers/__init__.py +9 -1
- package/skmemory/importers/telegram.py +384 -47
- package/skmemory/importers/telegram_api.py +580 -0
- package/skmemory/journal.py +7 -9
- package/skmemory/lovenote.py +8 -13
- package/skmemory/mcp_server.py +859 -0
- package/skmemory/models.py +51 -8
- package/skmemory/openclaw.py +20 -28
- package/skmemory/post_install.py +86 -0
- package/skmemory/predictive.py +236 -0
- package/skmemory/promotion.py +548 -0
- package/skmemory/quadrants.py +100 -24
- package/skmemory/register.py +580 -0
- package/skmemory/register_mcp.py +196 -0
- package/skmemory/ritual.py +224 -59
- package/skmemory/seeds.py +255 -11
- package/skmemory/setup_wizard.py +908 -0
- package/skmemory/sharing.py +408 -0
- package/skmemory/soul.py +98 -28
- package/skmemory/steelman.py +273 -260
- package/skmemory/store.py +411 -78
- package/skmemory/synthesis.py +634 -0
- package/skmemory/vault.py +225 -0
- package/tests/conftest.py +46 -0
- package/tests/integration/__init__.py +0 -0
- package/tests/integration/conftest.py +233 -0
- package/tests/integration/test_cross_backend.py +350 -0
- package/tests/integration/test_skgraph_live.py +420 -0
- package/tests/integration/test_skvector_live.py +366 -0
- package/tests/test_ai_client.py +1 -4
- package/tests/test_audience.py +233 -0
- package/tests/test_backup_rotation.py +318 -0
- package/tests/test_cli.py +6 -6
- package/tests/test_endpoint_selector.py +839 -0
- package/tests/test_export_import.py +4 -10
- package/tests/test_file_backend.py +0 -1
- package/tests/test_fortress.py +256 -0
- package/tests/test_fortress_hardening.py +441 -0
- package/tests/test_openclaw.py +6 -6
- package/tests/test_predictive.py +237 -0
- package/tests/test_promotion.py +347 -0
- package/tests/test_quadrants.py +11 -5
- package/tests/test_ritual.py +22 -18
- package/tests/test_seeds.py +97 -7
- package/tests/test_setup.py +950 -0
- package/tests/test_sharing.py +257 -0
- package/tests/test_skgraph_backend.py +660 -0
- package/tests/test_skvector_backend.py +326 -0
- package/tests/test_soul.py +1 -3
- package/tests/test_sqlite_backend.py +8 -17
- package/tests/test_steelman.py +7 -8
- package/tests/test_store.py +0 -2
- package/tests/test_store_graph_integration.py +245 -0
- package/tests/test_synthesis.py +275 -0
- package/tests/test_telegram_import.py +39 -15
- package/tests/test_vault.py +187 -0
- package/skmemory/backends/falkordb_backend.py +0 -310
|
@@ -0,0 +1,950 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for skmemory setup wizard, config persistence, and CLI commands.
|
|
3
|
+
|
|
4
|
+
All Docker/network operations are mocked — no Docker required to run tests.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import subprocess
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from unittest import mock
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
from click.testing import CliRunner
|
|
15
|
+
|
|
16
|
+
from skmemory.config import (
|
|
17
|
+
SKMemoryConfig,
|
|
18
|
+
load_config,
|
|
19
|
+
merge_env_and_config,
|
|
20
|
+
save_config,
|
|
21
|
+
)
|
|
22
|
+
from skmemory.setup_wizard import (
|
|
23
|
+
PlatformInfo,
|
|
24
|
+
check_port_available,
|
|
25
|
+
check_skgraph_health,
|
|
26
|
+
check_skvector_health,
|
|
27
|
+
compose_down,
|
|
28
|
+
compose_ps,
|
|
29
|
+
compose_up,
|
|
30
|
+
detect_platform,
|
|
31
|
+
find_compose_file,
|
|
32
|
+
get_docker_install_instructions,
|
|
33
|
+
install_python_deps,
|
|
34
|
+
run_setup_wizard,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# ═══════════════════════════════════════════════════════════
|
|
38
|
+
# Config tests
|
|
39
|
+
# ═══════════════════════════════════════════════════════════
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TestConfig:
|
|
43
|
+
"""Test SKMemoryConfig save/load/merge."""
|
|
44
|
+
|
|
45
|
+
def test_save_load_roundtrip(self, tmp_path: Path) -> None:
|
|
46
|
+
config = SKMemoryConfig(
|
|
47
|
+
skvector_url="http://localhost:6333",
|
|
48
|
+
skvector_key="secret",
|
|
49
|
+
skgraph_url="redis://localhost:6379",
|
|
50
|
+
backends_enabled=["skvector", "skgraph"],
|
|
51
|
+
docker_compose_file="/some/path/docker-compose.yml",
|
|
52
|
+
setup_completed_at="2026-02-28T12:00:00+00:00",
|
|
53
|
+
)
|
|
54
|
+
path = tmp_path / "config.yaml"
|
|
55
|
+
save_config(config, path)
|
|
56
|
+
|
|
57
|
+
loaded = load_config(path)
|
|
58
|
+
assert loaded is not None
|
|
59
|
+
assert loaded.skvector_url == "http://localhost:6333"
|
|
60
|
+
assert loaded.skvector_key == "secret"
|
|
61
|
+
assert loaded.skgraph_url == "redis://localhost:6379"
|
|
62
|
+
assert loaded.backends_enabled == ["skvector", "skgraph"]
|
|
63
|
+
assert loaded.setup_completed_at == "2026-02-28T12:00:00+00:00"
|
|
64
|
+
|
|
65
|
+
def test_load_missing_file(self, tmp_path: Path) -> None:
|
|
66
|
+
result = load_config(tmp_path / "nonexistent.yaml")
|
|
67
|
+
assert result is None
|
|
68
|
+
|
|
69
|
+
def test_load_invalid_yaml(self, tmp_path: Path) -> None:
|
|
70
|
+
path = tmp_path / "bad.yaml"
|
|
71
|
+
path.write_text("not: [valid: yaml: {{")
|
|
72
|
+
result = load_config(path)
|
|
73
|
+
# pyyaml may parse this or error — either way should not crash
|
|
74
|
+
assert result is None or isinstance(result, SKMemoryConfig)
|
|
75
|
+
|
|
76
|
+
def test_load_non_dict_yaml(self, tmp_path: Path) -> None:
|
|
77
|
+
path = tmp_path / "list.yaml"
|
|
78
|
+
path.write_text("- item1\n- item2\n")
|
|
79
|
+
result = load_config(path)
|
|
80
|
+
assert result is None
|
|
81
|
+
|
|
82
|
+
def test_save_creates_directory(self, tmp_path: Path) -> None:
|
|
83
|
+
nested = tmp_path / "deep" / "nested" / "config.yaml"
|
|
84
|
+
config = SKMemoryConfig(skvector_url="http://localhost:6333")
|
|
85
|
+
save_config(config, nested)
|
|
86
|
+
assert nested.exists()
|
|
87
|
+
|
|
88
|
+
def test_default_config_values(self) -> None:
|
|
89
|
+
config = SKMemoryConfig()
|
|
90
|
+
assert config.skvector_url is None
|
|
91
|
+
assert config.skvector_key is None
|
|
92
|
+
assert config.skgraph_url is None
|
|
93
|
+
assert config.backends_enabled == []
|
|
94
|
+
assert config.docker_compose_file is None
|
|
95
|
+
assert config.setup_completed_at is None
|
|
96
|
+
|
|
97
|
+
def test_merge_cli_overrides_env(
|
|
98
|
+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
99
|
+
) -> None:
|
|
100
|
+
monkeypatch.setenv("SKMEMORY_SKVECTOR_URL", "http://env:6333")
|
|
101
|
+
monkeypatch.setenv("SKMEMORY_SKVECTOR_KEY", "env-key")
|
|
102
|
+
monkeypatch.setenv("SKMEMORY_SKGRAPH_URL", "redis://env:6379")
|
|
103
|
+
|
|
104
|
+
skvector_url, skvector_key, skgraph_url = merge_env_and_config(
|
|
105
|
+
cli_skvector_url="http://cli:6333",
|
|
106
|
+
cli_skvector_key="cli-key",
|
|
107
|
+
cli_skgraph_url="redis://cli:6379",
|
|
108
|
+
)
|
|
109
|
+
assert skvector_url == "http://cli:6333"
|
|
110
|
+
assert skvector_key == "cli-key"
|
|
111
|
+
assert skgraph_url == "redis://cli:6379"
|
|
112
|
+
|
|
113
|
+
def test_merge_env_overrides_config(
|
|
114
|
+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
115
|
+
) -> None:
|
|
116
|
+
monkeypatch.setenv("SKMEMORY_SKVECTOR_URL", "http://env:6333")
|
|
117
|
+
monkeypatch.delenv("SKMEMORY_SKVECTOR_KEY", raising=False)
|
|
118
|
+
monkeypatch.delenv("SKMEMORY_SKGRAPH_URL", raising=False)
|
|
119
|
+
|
|
120
|
+
# Mock load_config to return a saved config
|
|
121
|
+
cfg = SKMemoryConfig(
|
|
122
|
+
skvector_url="http://config:6333",
|
|
123
|
+
skvector_key="config-key",
|
|
124
|
+
skgraph_url="redis://config:6379",
|
|
125
|
+
)
|
|
126
|
+
with mock.patch("skmemory.config.load_config", return_value=cfg):
|
|
127
|
+
skvector_url, skvector_key, skgraph_url = merge_env_and_config()
|
|
128
|
+
|
|
129
|
+
assert skvector_url == "http://env:6333" # env wins
|
|
130
|
+
assert skvector_key == "config-key" # falls through to config
|
|
131
|
+
assert skgraph_url == "redis://config:6379" # falls through to config
|
|
132
|
+
|
|
133
|
+
def test_merge_config_fallback(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
134
|
+
monkeypatch.delenv("SKMEMORY_SKVECTOR_URL", raising=False)
|
|
135
|
+
monkeypatch.delenv("SKMEMORY_SKVECTOR_KEY", raising=False)
|
|
136
|
+
monkeypatch.delenv("SKMEMORY_SKGRAPH_URL", raising=False)
|
|
137
|
+
|
|
138
|
+
cfg = SKMemoryConfig(
|
|
139
|
+
skvector_url="http://config:6333",
|
|
140
|
+
skgraph_url="redis://config:6379",
|
|
141
|
+
)
|
|
142
|
+
with mock.patch("skmemory.config.load_config", return_value=cfg):
|
|
143
|
+
skvector_url, skvector_key, skgraph_url = merge_env_and_config()
|
|
144
|
+
|
|
145
|
+
assert skvector_url == "http://config:6333"
|
|
146
|
+
assert skvector_key is None
|
|
147
|
+
assert skgraph_url == "redis://config:6379"
|
|
148
|
+
|
|
149
|
+
def test_merge_all_none(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
150
|
+
monkeypatch.delenv("SKMEMORY_SKVECTOR_URL", raising=False)
|
|
151
|
+
monkeypatch.delenv("SKMEMORY_SKVECTOR_KEY", raising=False)
|
|
152
|
+
monkeypatch.delenv("SKMEMORY_SKGRAPH_URL", raising=False)
|
|
153
|
+
|
|
154
|
+
with mock.patch("skmemory.config.load_config", return_value=None):
|
|
155
|
+
skvector_url, skvector_key, skgraph_url = merge_env_and_config()
|
|
156
|
+
|
|
157
|
+
assert skvector_url is None
|
|
158
|
+
assert skvector_key is None
|
|
159
|
+
assert skgraph_url is None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# ═══════════════════════════════════════════════════════════
|
|
163
|
+
# Platform detection tests
|
|
164
|
+
# ═══════════════════════════════════════════════════════════
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class TestPlatformDetection:
|
|
168
|
+
"""Test Docker/Compose detection across platforms."""
|
|
169
|
+
|
|
170
|
+
def test_docker_not_found(self) -> None:
|
|
171
|
+
with mock.patch("skmemory.setup_wizard.shutil.which", return_value=None):
|
|
172
|
+
info = detect_platform()
|
|
173
|
+
assert not info.docker_available
|
|
174
|
+
assert not info.compose_available
|
|
175
|
+
|
|
176
|
+
def test_docker_daemon_not_running(self) -> None:
|
|
177
|
+
with (
|
|
178
|
+
mock.patch("skmemory.setup_wizard.shutil.which", return_value="/usr/bin/docker"),
|
|
179
|
+
mock.patch(
|
|
180
|
+
"skmemory.setup_wizard.subprocess.run",
|
|
181
|
+
return_value=subprocess.CompletedProcess(
|
|
182
|
+
args=["docker", "info"], returncode=1, stdout="", stderr="Cannot connect"
|
|
183
|
+
),
|
|
184
|
+
),
|
|
185
|
+
):
|
|
186
|
+
info = detect_platform()
|
|
187
|
+
assert not info.docker_available
|
|
188
|
+
|
|
189
|
+
def test_compose_v2_detected(self) -> None:
|
|
190
|
+
def mock_run(cmd, **kwargs):
|
|
191
|
+
if cmd == ["docker", "info"]:
|
|
192
|
+
return subprocess.CompletedProcess(args=cmd, returncode=0)
|
|
193
|
+
if cmd == ["docker", "version", "--format", "{{.Server.Version}}"]:
|
|
194
|
+
return subprocess.CompletedProcess(
|
|
195
|
+
args=cmd, returncode=0, stdout="24.0.7\n", stderr=""
|
|
196
|
+
)
|
|
197
|
+
if cmd == ["docker", "compose", "version"]:
|
|
198
|
+
return subprocess.CompletedProcess(
|
|
199
|
+
args=cmd, returncode=0, stdout="v2.23.0\n", stderr=""
|
|
200
|
+
)
|
|
201
|
+
return subprocess.CompletedProcess(args=cmd, returncode=1)
|
|
202
|
+
|
|
203
|
+
with (
|
|
204
|
+
mock.patch("skmemory.setup_wizard.shutil.which", return_value="/usr/bin/docker"),
|
|
205
|
+
mock.patch("skmemory.setup_wizard.subprocess.run", side_effect=mock_run),
|
|
206
|
+
):
|
|
207
|
+
info = detect_platform()
|
|
208
|
+
|
|
209
|
+
assert info.docker_available
|
|
210
|
+
assert info.compose_available
|
|
211
|
+
assert not info.compose_legacy
|
|
212
|
+
assert info.docker_version == "24.0.7"
|
|
213
|
+
|
|
214
|
+
def test_compose_v1_fallback(self) -> None:
|
|
215
|
+
def mock_run(cmd, **kwargs):
|
|
216
|
+
if cmd == ["docker", "info"]:
|
|
217
|
+
return subprocess.CompletedProcess(args=cmd, returncode=0)
|
|
218
|
+
if cmd == ["docker", "version", "--format", "{{.Server.Version}}"]:
|
|
219
|
+
return subprocess.CompletedProcess(
|
|
220
|
+
args=cmd, returncode=0, stdout="20.10.0\n", stderr=""
|
|
221
|
+
)
|
|
222
|
+
if cmd == ["docker", "compose", "version"]:
|
|
223
|
+
return subprocess.CompletedProcess(args=cmd, returncode=1)
|
|
224
|
+
if cmd == ["docker-compose", "--version"]:
|
|
225
|
+
return subprocess.CompletedProcess(
|
|
226
|
+
args=cmd, returncode=0, stdout="docker-compose version 1.29.2\n", stderr=""
|
|
227
|
+
)
|
|
228
|
+
return subprocess.CompletedProcess(args=cmd, returncode=1)
|
|
229
|
+
|
|
230
|
+
with (
|
|
231
|
+
mock.patch("skmemory.setup_wizard.shutil.which", return_value="/usr/bin/docker"),
|
|
232
|
+
mock.patch("skmemory.setup_wizard.subprocess.run", side_effect=mock_run),
|
|
233
|
+
):
|
|
234
|
+
info = detect_platform()
|
|
235
|
+
|
|
236
|
+
assert info.docker_available
|
|
237
|
+
assert info.compose_available
|
|
238
|
+
assert info.compose_legacy
|
|
239
|
+
|
|
240
|
+
def test_no_compose_at_all(self) -> None:
|
|
241
|
+
def mock_run(cmd, **kwargs):
|
|
242
|
+
if cmd == ["docker", "info"]:
|
|
243
|
+
return subprocess.CompletedProcess(args=cmd, returncode=0)
|
|
244
|
+
if cmd == ["docker", "version", "--format", "{{.Server.Version}}"]:
|
|
245
|
+
return subprocess.CompletedProcess(
|
|
246
|
+
args=cmd, returncode=0, stdout="24.0.7\n", stderr=""
|
|
247
|
+
)
|
|
248
|
+
if cmd == ["docker", "compose", "version"]:
|
|
249
|
+
return subprocess.CompletedProcess(args=cmd, returncode=1)
|
|
250
|
+
if cmd == ["docker-compose", "--version"]:
|
|
251
|
+
return subprocess.CompletedProcess(args=cmd, returncode=1)
|
|
252
|
+
return subprocess.CompletedProcess(args=cmd, returncode=1)
|
|
253
|
+
|
|
254
|
+
with (
|
|
255
|
+
mock.patch("skmemory.setup_wizard.shutil.which", return_value="/usr/bin/docker"),
|
|
256
|
+
mock.patch("skmemory.setup_wizard.subprocess.run", side_effect=mock_run),
|
|
257
|
+
):
|
|
258
|
+
info = detect_platform()
|
|
259
|
+
|
|
260
|
+
assert info.docker_available
|
|
261
|
+
assert not info.compose_available
|
|
262
|
+
|
|
263
|
+
def test_docker_install_instructions_linux(self) -> None:
|
|
264
|
+
msg = get_docker_install_instructions("Linux")
|
|
265
|
+
assert "get.docker.com" in msg
|
|
266
|
+
|
|
267
|
+
def test_docker_install_instructions_mac(self) -> None:
|
|
268
|
+
msg = get_docker_install_instructions("Darwin")
|
|
269
|
+
assert "mac" in msg.lower() or "Docker Desktop" in msg
|
|
270
|
+
|
|
271
|
+
def test_docker_install_instructions_windows(self) -> None:
|
|
272
|
+
msg = get_docker_install_instructions("Windows")
|
|
273
|
+
assert "windows" in msg.lower() or "WSL" in msg
|
|
274
|
+
|
|
275
|
+
def test_docker_install_instructions_unknown(self) -> None:
|
|
276
|
+
msg = get_docker_install_instructions("FreeBSD")
|
|
277
|
+
assert "docker" in msg.lower()
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# ═══════════════════════════════════════════════════════════
|
|
281
|
+
# Port check tests
|
|
282
|
+
# ═══════════════════════════════════════════════════════════
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class TestPortCheck:
|
|
286
|
+
"""Test port availability detection."""
|
|
287
|
+
|
|
288
|
+
def test_port_available(self) -> None:
|
|
289
|
+
mock_sock = mock.MagicMock()
|
|
290
|
+
mock_sock.__enter__ = mock.MagicMock(return_value=mock_sock)
|
|
291
|
+
mock_sock.__exit__ = mock.MagicMock(return_value=False)
|
|
292
|
+
with mock.patch("skmemory.setup_wizard.socket.socket", return_value=mock_sock):
|
|
293
|
+
assert check_port_available(6333) is True
|
|
294
|
+
mock_sock.bind.assert_called_once_with(("127.0.0.1", 6333))
|
|
295
|
+
|
|
296
|
+
def test_port_occupied(self) -> None:
|
|
297
|
+
mock_sock = mock.MagicMock()
|
|
298
|
+
mock_sock.__enter__ = mock.MagicMock(return_value=mock_sock)
|
|
299
|
+
mock_sock.__exit__ = mock.MagicMock(return_value=False)
|
|
300
|
+
mock_sock.bind.side_effect = OSError("Address already in use")
|
|
301
|
+
with mock.patch("skmemory.setup_wizard.socket.socket", return_value=mock_sock):
|
|
302
|
+
assert check_port_available(6333) is False
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# ═══════════════════════════════════════════════════════════
|
|
306
|
+
# Health check tests
|
|
307
|
+
# ═══════════════════════════════════════════════════════════
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class TestHealthChecks:
|
|
311
|
+
"""Test SKVector and SKGraph health checks (mocked network)."""
|
|
312
|
+
|
|
313
|
+
def test_skvector_healthy(self) -> None:
|
|
314
|
+
mock_resp = mock.MagicMock()
|
|
315
|
+
mock_resp.status = 200
|
|
316
|
+
mock_resp.__enter__ = mock.MagicMock(return_value=mock_resp)
|
|
317
|
+
mock_resp.__exit__ = mock.MagicMock(return_value=False)
|
|
318
|
+
|
|
319
|
+
with mock.patch("skmemory.setup_wizard.urllib.request.urlopen", return_value=mock_resp):
|
|
320
|
+
assert check_skvector_health(timeout=5) is True
|
|
321
|
+
|
|
322
|
+
def test_skvector_timeout(self) -> None:
|
|
323
|
+
with (
|
|
324
|
+
mock.patch(
|
|
325
|
+
"skmemory.setup_wizard.urllib.request.urlopen",
|
|
326
|
+
side_effect=urllib.error.URLError("Connection refused"),
|
|
327
|
+
),
|
|
328
|
+
mock.patch("skmemory.setup_wizard.time.sleep"),
|
|
329
|
+
):
|
|
330
|
+
assert check_skvector_health(timeout=1) is False
|
|
331
|
+
|
|
332
|
+
def test_skgraph_healthy(self) -> None:
|
|
333
|
+
mock_sock = mock.MagicMock()
|
|
334
|
+
mock_sock.recv.return_value = b"+PONG\r\n"
|
|
335
|
+
mock_sock.__enter__ = mock.MagicMock(return_value=mock_sock)
|
|
336
|
+
mock_sock.__exit__ = mock.MagicMock(return_value=False)
|
|
337
|
+
|
|
338
|
+
with mock.patch("skmemory.setup_wizard.socket.create_connection", return_value=mock_sock):
|
|
339
|
+
assert check_skgraph_health(timeout=5) is True
|
|
340
|
+
|
|
341
|
+
def test_skgraph_timeout(self) -> None:
|
|
342
|
+
with (
|
|
343
|
+
mock.patch(
|
|
344
|
+
"skmemory.setup_wizard.socket.create_connection",
|
|
345
|
+
side_effect=OSError("Connection refused"),
|
|
346
|
+
),
|
|
347
|
+
mock.patch("skmemory.setup_wizard.time.sleep"),
|
|
348
|
+
):
|
|
349
|
+
assert check_skgraph_health(timeout=1) is False
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
# ═══════════════════════════════════════════════════════════
|
|
353
|
+
# Compose command tests
|
|
354
|
+
# ═══════════════════════════════════════════════════════════
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class TestComposeCommands:
|
|
358
|
+
"""Test docker compose subprocess invocations."""
|
|
359
|
+
|
|
360
|
+
def test_compose_up_v2(self, tmp_path: Path) -> None:
|
|
361
|
+
compose_file = tmp_path / "docker-compose.yml"
|
|
362
|
+
compose_file.write_text("services: {}")
|
|
363
|
+
|
|
364
|
+
with mock.patch("skmemory.setup_wizard.subprocess.run") as mock_run:
|
|
365
|
+
mock_run.return_value = subprocess.CompletedProcess(
|
|
366
|
+
args=[], returncode=0, stdout="", stderr=""
|
|
367
|
+
)
|
|
368
|
+
compose_up(services=["skvector"], compose_file=compose_file, use_legacy=False)
|
|
369
|
+
|
|
370
|
+
mock_run.assert_called_once()
|
|
371
|
+
args = mock_run.call_args[0][0]
|
|
372
|
+
assert args[:2] == ["docker", "compose"]
|
|
373
|
+
assert "-f" in args
|
|
374
|
+
assert "up" in args
|
|
375
|
+
assert "-d" in args
|
|
376
|
+
assert "skvector" in args
|
|
377
|
+
|
|
378
|
+
def test_compose_up_legacy(self, tmp_path: Path) -> None:
|
|
379
|
+
compose_file = tmp_path / "docker-compose.yml"
|
|
380
|
+
compose_file.write_text("services: {}")
|
|
381
|
+
|
|
382
|
+
with mock.patch("skmemory.setup_wizard.subprocess.run") as mock_run:
|
|
383
|
+
mock_run.return_value = subprocess.CompletedProcess(
|
|
384
|
+
args=[], returncode=0, stdout="", stderr=""
|
|
385
|
+
)
|
|
386
|
+
compose_up(services=None, compose_file=compose_file, use_legacy=True)
|
|
387
|
+
|
|
388
|
+
args = mock_run.call_args[0][0]
|
|
389
|
+
assert args[0] == "docker-compose"
|
|
390
|
+
|
|
391
|
+
def test_compose_down_with_volumes(self, tmp_path: Path) -> None:
|
|
392
|
+
compose_file = tmp_path / "docker-compose.yml"
|
|
393
|
+
compose_file.write_text("services: {}")
|
|
394
|
+
|
|
395
|
+
with mock.patch("skmemory.setup_wizard.subprocess.run") as mock_run:
|
|
396
|
+
mock_run.return_value = subprocess.CompletedProcess(
|
|
397
|
+
args=[], returncode=0, stdout="", stderr=""
|
|
398
|
+
)
|
|
399
|
+
compose_down(compose_file=compose_file, remove_volumes=True, use_legacy=False)
|
|
400
|
+
|
|
401
|
+
args = mock_run.call_args[0][0]
|
|
402
|
+
assert "down" in args
|
|
403
|
+
assert "-v" in args
|
|
404
|
+
|
|
405
|
+
def test_compose_down_without_volumes(self, tmp_path: Path) -> None:
|
|
406
|
+
compose_file = tmp_path / "docker-compose.yml"
|
|
407
|
+
compose_file.write_text("services: {}")
|
|
408
|
+
|
|
409
|
+
with mock.patch("skmemory.setup_wizard.subprocess.run") as mock_run:
|
|
410
|
+
mock_run.return_value = subprocess.CompletedProcess(
|
|
411
|
+
args=[], returncode=0, stdout="", stderr=""
|
|
412
|
+
)
|
|
413
|
+
compose_down(compose_file=compose_file, remove_volumes=False, use_legacy=False)
|
|
414
|
+
|
|
415
|
+
args = mock_run.call_args[0][0]
|
|
416
|
+
assert "down" in args
|
|
417
|
+
assert "-v" not in args
|
|
418
|
+
|
|
419
|
+
def test_compose_ps(self, tmp_path: Path) -> None:
|
|
420
|
+
compose_file = tmp_path / "docker-compose.yml"
|
|
421
|
+
compose_file.write_text("services: {}")
|
|
422
|
+
|
|
423
|
+
with mock.patch("skmemory.setup_wizard.subprocess.run") as mock_run:
|
|
424
|
+
mock_run.return_value = subprocess.CompletedProcess(
|
|
425
|
+
args=[], returncode=0, stdout="running", stderr=""
|
|
426
|
+
)
|
|
427
|
+
result = compose_ps(compose_file=compose_file, use_legacy=False)
|
|
428
|
+
|
|
429
|
+
assert result.stdout == "running"
|
|
430
|
+
args = mock_run.call_args[0][0]
|
|
431
|
+
assert "ps" in args
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
# ═══════════════════════════════════════════════════════════
|
|
435
|
+
# Pip install tests
|
|
436
|
+
# ═══════════════════════════════════════════════════════════
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
class TestPipInstall:
|
|
440
|
+
"""Test Python dependency installation."""
|
|
441
|
+
|
|
442
|
+
def test_install_skvector_deps(self) -> None:
|
|
443
|
+
with mock.patch("skmemory.setup_wizard.subprocess.run") as mock_run:
|
|
444
|
+
mock_run.return_value = subprocess.CompletedProcess(
|
|
445
|
+
args=[], returncode=0, stdout="", stderr=""
|
|
446
|
+
)
|
|
447
|
+
install_python_deps(["skvector"])
|
|
448
|
+
|
|
449
|
+
args = mock_run.call_args[0][0]
|
|
450
|
+
assert "pip" in " ".join(args)
|
|
451
|
+
assert "qdrant-client" in args
|
|
452
|
+
assert "sentence-transformers" in args
|
|
453
|
+
assert "falkordb" not in args
|
|
454
|
+
|
|
455
|
+
def test_install_skgraph_deps(self) -> None:
|
|
456
|
+
with mock.patch("skmemory.setup_wizard.subprocess.run") as mock_run:
|
|
457
|
+
mock_run.return_value = subprocess.CompletedProcess(
|
|
458
|
+
args=[], returncode=0, stdout="", stderr=""
|
|
459
|
+
)
|
|
460
|
+
install_python_deps(["skgraph"])
|
|
461
|
+
|
|
462
|
+
args = mock_run.call_args[0][0]
|
|
463
|
+
assert "falkordb" in args
|
|
464
|
+
assert "qdrant-client" not in args
|
|
465
|
+
|
|
466
|
+
def test_install_both_deps(self) -> None:
|
|
467
|
+
with mock.patch("skmemory.setup_wizard.subprocess.run") as mock_run:
|
|
468
|
+
mock_run.return_value = subprocess.CompletedProcess(
|
|
469
|
+
args=[], returncode=0, stdout="", stderr=""
|
|
470
|
+
)
|
|
471
|
+
install_python_deps(["skvector", "skgraph"])
|
|
472
|
+
|
|
473
|
+
args = mock_run.call_args[0][0]
|
|
474
|
+
assert "qdrant-client" in args
|
|
475
|
+
assert "sentence-transformers" in args
|
|
476
|
+
assert "falkordb" in args
|
|
477
|
+
|
|
478
|
+
def test_install_no_backends(self) -> None:
|
|
479
|
+
result = install_python_deps([])
|
|
480
|
+
assert result.returncode == 0
|
|
481
|
+
|
|
482
|
+
def test_install_failure(self) -> None:
|
|
483
|
+
with mock.patch("skmemory.setup_wizard.subprocess.run") as mock_run:
|
|
484
|
+
mock_run.return_value = subprocess.CompletedProcess(
|
|
485
|
+
args=[], returncode=1, stdout="", stderr="ERROR: No matching distribution"
|
|
486
|
+
)
|
|
487
|
+
result = install_python_deps(["skvector"])
|
|
488
|
+
assert result.returncode == 1
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
# ═══════════════════════════════════════════════════════════
|
|
492
|
+
# Find compose file tests
|
|
493
|
+
# ═══════════════════════════════════════════════════════════
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
class TestFindComposeFile:
|
|
497
|
+
"""Test compose file discovery."""
|
|
498
|
+
|
|
499
|
+
def test_finds_bundled_compose(self) -> None:
|
|
500
|
+
# The real docker-compose.yml exists in the package
|
|
501
|
+
path = find_compose_file()
|
|
502
|
+
assert path.exists()
|
|
503
|
+
assert path.name == "docker-compose.yml"
|
|
504
|
+
|
|
505
|
+
def test_fallback_generates_compose(
|
|
506
|
+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
507
|
+
) -> None:
|
|
508
|
+
# Trick find_compose_file into not finding the bundled one
|
|
509
|
+
monkeypatch.setattr(
|
|
510
|
+
"skmemory.setup_wizard.Path.__file__",
|
|
511
|
+
str(tmp_path / "fake" / "setup_wizard.py"),
|
|
512
|
+
raising=False,
|
|
513
|
+
)
|
|
514
|
+
# Use CONFIG_DIR override to avoid touching real ~/.skmemory
|
|
515
|
+
monkeypatch.setattr("skmemory.setup_wizard.CONFIG_DIR", tmp_path)
|
|
516
|
+
|
|
517
|
+
# Make the bundled path not exist by patching
|
|
518
|
+
fake_package = tmp_path / "nonexistent_pkg"
|
|
519
|
+
with mock.patch(
|
|
520
|
+
"skmemory.setup_wizard.Path.__file__", str(fake_package / "setup_wizard.py")
|
|
521
|
+
):
|
|
522
|
+
# Just verify the function doesn't crash when it can't find bundled
|
|
523
|
+
path = find_compose_file()
|
|
524
|
+
assert path.exists()
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
# ═══════════════════════════════════════════════════════════
|
|
528
|
+
# Setup wizard integration test (all mocked)
|
|
529
|
+
# ═══════════════════════════════════════════════════════════
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
class TestSetupWizard:
|
|
533
|
+
"""Test the full setup wizard flow with all externals mocked."""
|
|
534
|
+
|
|
535
|
+
def _mock_platform(self, docker=True, compose=True, legacy=False, version="24.0.7"):
|
|
536
|
+
return PlatformInfo(
|
|
537
|
+
os_name="Linux",
|
|
538
|
+
docker_available=docker,
|
|
539
|
+
compose_available=compose,
|
|
540
|
+
compose_legacy=legacy,
|
|
541
|
+
docker_version=version,
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
def test_wizard_success(self, tmp_path: Path) -> None:
|
|
545
|
+
output = []
|
|
546
|
+
|
|
547
|
+
with (
|
|
548
|
+
mock.patch(
|
|
549
|
+
"skmemory.setup_wizard.detect_platform", return_value=self._mock_platform()
|
|
550
|
+
),
|
|
551
|
+
mock.patch("skmemory.setup_wizard.check_port_available", return_value=True),
|
|
552
|
+
mock.patch(
|
|
553
|
+
"skmemory.setup_wizard.find_compose_file",
|
|
554
|
+
return_value=tmp_path / "docker-compose.yml",
|
|
555
|
+
),
|
|
556
|
+
mock.patch(
|
|
557
|
+
"skmemory.setup_wizard.compose_up",
|
|
558
|
+
return_value=subprocess.CompletedProcess(
|
|
559
|
+
args=[], returncode=0, stdout="", stderr=""
|
|
560
|
+
),
|
|
561
|
+
),
|
|
562
|
+
mock.patch("skmemory.setup_wizard.check_skvector_health", return_value=True),
|
|
563
|
+
mock.patch("skmemory.setup_wizard.check_skgraph_health", return_value=True),
|
|
564
|
+
mock.patch(
|
|
565
|
+
"skmemory.setup_wizard.install_python_deps",
|
|
566
|
+
return_value=subprocess.CompletedProcess(
|
|
567
|
+
args=[], returncode=0, stdout="", stderr=""
|
|
568
|
+
),
|
|
569
|
+
),
|
|
570
|
+
mock.patch("skmemory.setup_wizard.save_config", return_value=tmp_path / "config.yaml"),
|
|
571
|
+
):
|
|
572
|
+
result = run_setup_wizard(
|
|
573
|
+
enable_skvector=True,
|
|
574
|
+
enable_skgraph=True,
|
|
575
|
+
skip_deps=False,
|
|
576
|
+
non_interactive=True,
|
|
577
|
+
echo=output.append,
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
assert result["success"] is True
|
|
581
|
+
assert "skvector" in result["services"]
|
|
582
|
+
assert "skgraph" in result["services"]
|
|
583
|
+
assert result["health"]["skvector"] is True
|
|
584
|
+
assert result["health"]["skgraph"] is True
|
|
585
|
+
|
|
586
|
+
def test_wizard_no_docker(self) -> None:
|
|
587
|
+
output = []
|
|
588
|
+
with mock.patch(
|
|
589
|
+
"skmemory.setup_wizard.detect_platform",
|
|
590
|
+
return_value=self._mock_platform(docker=False),
|
|
591
|
+
):
|
|
592
|
+
result = run_setup_wizard(non_interactive=True, echo=output.append)
|
|
593
|
+
|
|
594
|
+
assert result["success"] is False
|
|
595
|
+
assert "Docker not available" in result["errors"]
|
|
596
|
+
|
|
597
|
+
def test_wizard_no_compose(self) -> None:
|
|
598
|
+
output = []
|
|
599
|
+
with mock.patch(
|
|
600
|
+
"skmemory.setup_wizard.detect_platform",
|
|
601
|
+
return_value=self._mock_platform(compose=False),
|
|
602
|
+
):
|
|
603
|
+
result = run_setup_wizard(non_interactive=True, echo=output.append)
|
|
604
|
+
|
|
605
|
+
assert result["success"] is False
|
|
606
|
+
assert "Docker Compose not available" in result["errors"]
|
|
607
|
+
|
|
608
|
+
def test_wizard_port_conflict(self) -> None:
|
|
609
|
+
output = []
|
|
610
|
+
with (
|
|
611
|
+
mock.patch(
|
|
612
|
+
"skmemory.setup_wizard.detect_platform", return_value=self._mock_platform()
|
|
613
|
+
),
|
|
614
|
+
mock.patch("skmemory.setup_wizard.check_port_available", return_value=False),
|
|
615
|
+
):
|
|
616
|
+
result = run_setup_wizard(non_interactive=True, echo=output.append)
|
|
617
|
+
|
|
618
|
+
assert result["success"] is False
|
|
619
|
+
assert any("Port conflicts" in e for e in result["errors"])
|
|
620
|
+
|
|
621
|
+
def test_wizard_compose_up_fails(self, tmp_path: Path) -> None:
|
|
622
|
+
output = []
|
|
623
|
+
with (
|
|
624
|
+
mock.patch(
|
|
625
|
+
"skmemory.setup_wizard.detect_platform", return_value=self._mock_platform()
|
|
626
|
+
),
|
|
627
|
+
mock.patch("skmemory.setup_wizard.check_port_available", return_value=True),
|
|
628
|
+
mock.patch(
|
|
629
|
+
"skmemory.setup_wizard.find_compose_file", return_value=tmp_path / "dc.yml"
|
|
630
|
+
),
|
|
631
|
+
mock.patch(
|
|
632
|
+
"skmemory.setup_wizard.compose_up",
|
|
633
|
+
return_value=subprocess.CompletedProcess(
|
|
634
|
+
args=[], returncode=1, stdout="", stderr="image pull failed"
|
|
635
|
+
),
|
|
636
|
+
),
|
|
637
|
+
):
|
|
638
|
+
result = run_setup_wizard(non_interactive=True, echo=output.append)
|
|
639
|
+
|
|
640
|
+
assert result["success"] is False
|
|
641
|
+
assert any("docker compose up failed" in e for e in result["errors"])
|
|
642
|
+
|
|
643
|
+
def test_wizard_skvector_only(self, tmp_path: Path) -> None:
|
|
644
|
+
output = []
|
|
645
|
+
with (
|
|
646
|
+
mock.patch(
|
|
647
|
+
"skmemory.setup_wizard.detect_platform", return_value=self._mock_platform()
|
|
648
|
+
),
|
|
649
|
+
mock.patch("skmemory.setup_wizard.check_port_available", return_value=True),
|
|
650
|
+
mock.patch(
|
|
651
|
+
"skmemory.setup_wizard.find_compose_file", return_value=tmp_path / "dc.yml"
|
|
652
|
+
),
|
|
653
|
+
mock.patch(
|
|
654
|
+
"skmemory.setup_wizard.compose_up",
|
|
655
|
+
return_value=subprocess.CompletedProcess(
|
|
656
|
+
args=[], returncode=0, stdout="", stderr=""
|
|
657
|
+
),
|
|
658
|
+
),
|
|
659
|
+
mock.patch("skmemory.setup_wizard.check_skvector_health", return_value=True),
|
|
660
|
+
mock.patch(
|
|
661
|
+
"skmemory.setup_wizard.install_python_deps",
|
|
662
|
+
return_value=subprocess.CompletedProcess(
|
|
663
|
+
args=[], returncode=0, stdout="", stderr=""
|
|
664
|
+
),
|
|
665
|
+
),
|
|
666
|
+
mock.patch("skmemory.setup_wizard.save_config", return_value=tmp_path / "config.yaml"),
|
|
667
|
+
):
|
|
668
|
+
result = run_setup_wizard(
|
|
669
|
+
enable_skvector=True,
|
|
670
|
+
enable_skgraph=False,
|
|
671
|
+
non_interactive=True,
|
|
672
|
+
echo=output.append,
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
assert result["success"] is True
|
|
676
|
+
assert result["services"] == ["skvector"]
|
|
677
|
+
assert "skgraph" not in result["health"]
|
|
678
|
+
|
|
679
|
+
def test_wizard_no_backends_selected(self) -> None:
|
|
680
|
+
output = []
|
|
681
|
+
with mock.patch(
|
|
682
|
+
"skmemory.setup_wizard.detect_platform", return_value=self._mock_platform()
|
|
683
|
+
):
|
|
684
|
+
result = run_setup_wizard(
|
|
685
|
+
enable_skvector=False,
|
|
686
|
+
enable_skgraph=False,
|
|
687
|
+
non_interactive=True,
|
|
688
|
+
echo=output.append,
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
assert result["success"] is True
|
|
692
|
+
assert result["services"] == []
|
|
693
|
+
|
|
694
|
+
def test_wizard_skip_deps(self, tmp_path: Path) -> None:
|
|
695
|
+
output = []
|
|
696
|
+
with (
|
|
697
|
+
mock.patch(
|
|
698
|
+
"skmemory.setup_wizard.detect_platform", return_value=self._mock_platform()
|
|
699
|
+
),
|
|
700
|
+
mock.patch("skmemory.setup_wizard.check_port_available", return_value=True),
|
|
701
|
+
mock.patch(
|
|
702
|
+
"skmemory.setup_wizard.find_compose_file", return_value=tmp_path / "dc.yml"
|
|
703
|
+
),
|
|
704
|
+
mock.patch(
|
|
705
|
+
"skmemory.setup_wizard.compose_up",
|
|
706
|
+
return_value=subprocess.CompletedProcess(
|
|
707
|
+
args=[], returncode=0, stdout="", stderr=""
|
|
708
|
+
),
|
|
709
|
+
),
|
|
710
|
+
mock.patch("skmemory.setup_wizard.check_skvector_health", return_value=True),
|
|
711
|
+
mock.patch("skmemory.setup_wizard.install_python_deps") as mock_pip,
|
|
712
|
+
mock.patch("skmemory.setup_wizard.save_config", return_value=tmp_path / "config.yaml"),
|
|
713
|
+
):
|
|
714
|
+
result = run_setup_wizard(
|
|
715
|
+
enable_skvector=True,
|
|
716
|
+
enable_skgraph=False,
|
|
717
|
+
skip_deps=True,
|
|
718
|
+
non_interactive=True,
|
|
719
|
+
echo=output.append,
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
assert result["success"] is True
|
|
723
|
+
mock_pip.assert_not_called()
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
# ═══════════════════════════════════════════════════════════
|
|
727
|
+
# CLI tests
|
|
728
|
+
# ═══════════════════════════════════════════════════════════
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
class TestSetupCLI:
|
|
732
|
+
"""Test the setup CLI commands via Click's CliRunner."""
|
|
733
|
+
|
|
734
|
+
def test_setup_wizard_cli(self) -> None:
|
|
735
|
+
from skmemory.cli import cli
|
|
736
|
+
|
|
737
|
+
runner = CliRunner()
|
|
738
|
+
with mock.patch("skmemory.setup_wizard.run_setup_wizard") as mock_wizard:
|
|
739
|
+
mock_wizard.return_value = {
|
|
740
|
+
"success": True,
|
|
741
|
+
"services": [],
|
|
742
|
+
"health": {},
|
|
743
|
+
"config_path": None,
|
|
744
|
+
"errors": [],
|
|
745
|
+
}
|
|
746
|
+
result = runner.invoke(cli, ["setup", "wizard", "--yes", "--no-skgraph"])
|
|
747
|
+
|
|
748
|
+
assert result.exit_code == 0
|
|
749
|
+
mock_wizard.assert_called_once()
|
|
750
|
+
call_kwargs = mock_wizard.call_args
|
|
751
|
+
assert (
|
|
752
|
+
call_kwargs.kwargs.get("enable_skgraph") is False
|
|
753
|
+
or call_kwargs[1].get("enable_skgraph") is False
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
def test_setup_wizard_cli_failure(self) -> None:
|
|
757
|
+
from skmemory.cli import cli
|
|
758
|
+
|
|
759
|
+
runner = CliRunner()
|
|
760
|
+
with mock.patch("skmemory.setup_wizard.run_setup_wizard") as mock_wizard:
|
|
761
|
+
mock_wizard.return_value = {
|
|
762
|
+
"success": False,
|
|
763
|
+
"services": [],
|
|
764
|
+
"health": {},
|
|
765
|
+
"config_path": None,
|
|
766
|
+
"errors": ["Docker not available"],
|
|
767
|
+
}
|
|
768
|
+
result = runner.invoke(cli, ["setup", "wizard", "--yes"])
|
|
769
|
+
|
|
770
|
+
assert result.exit_code == 1
|
|
771
|
+
|
|
772
|
+
def test_setup_status_no_config(self) -> None:
|
|
773
|
+
from skmemory.cli import cli
|
|
774
|
+
|
|
775
|
+
runner = CliRunner()
|
|
776
|
+
with mock.patch("skmemory.config.load_config", return_value=None):
|
|
777
|
+
result = runner.invoke(cli, ["setup", "status"])
|
|
778
|
+
|
|
779
|
+
assert "No setup config found" in result.output
|
|
780
|
+
|
|
781
|
+
def test_setup_status_with_config(self) -> None:
|
|
782
|
+
from skmemory.cli import cli
|
|
783
|
+
|
|
784
|
+
cfg = SKMemoryConfig(
|
|
785
|
+
skvector_url="http://localhost:6333",
|
|
786
|
+
backends_enabled=["skvector"],
|
|
787
|
+
setup_completed_at="2026-02-28T12:00:00+00:00",
|
|
788
|
+
)
|
|
789
|
+
runner = CliRunner()
|
|
790
|
+
with (
|
|
791
|
+
mock.patch("skmemory.config.load_config", return_value=cfg),
|
|
792
|
+
mock.patch(
|
|
793
|
+
"skmemory.setup_wizard.detect_platform",
|
|
794
|
+
return_value=PlatformInfo(
|
|
795
|
+
os_name="Linux", docker_available=True, compose_available=True
|
|
796
|
+
),
|
|
797
|
+
),
|
|
798
|
+
mock.patch(
|
|
799
|
+
"skmemory.setup_wizard.compose_ps",
|
|
800
|
+
return_value=subprocess.CompletedProcess(
|
|
801
|
+
args=[], returncode=0, stdout="skmemory-skvector running", stderr=""
|
|
802
|
+
),
|
|
803
|
+
),
|
|
804
|
+
mock.patch("skmemory.setup_wizard.check_skvector_health", return_value=True),
|
|
805
|
+
):
|
|
806
|
+
result = runner.invoke(cli, ["setup", "status"])
|
|
807
|
+
|
|
808
|
+
assert "skvector" in result.output.lower()
|
|
809
|
+
|
|
810
|
+
def test_setup_start_cli(self) -> None:
|
|
811
|
+
from skmemory.cli import cli
|
|
812
|
+
|
|
813
|
+
runner = CliRunner()
|
|
814
|
+
with (
|
|
815
|
+
mock.patch(
|
|
816
|
+
"skmemory.config.load_config",
|
|
817
|
+
return_value=SKMemoryConfig(backends_enabled=["skvector"]),
|
|
818
|
+
),
|
|
819
|
+
mock.patch(
|
|
820
|
+
"skmemory.setup_wizard.detect_platform",
|
|
821
|
+
return_value=PlatformInfo(
|
|
822
|
+
os_name="Linux", docker_available=True, compose_available=True
|
|
823
|
+
),
|
|
824
|
+
),
|
|
825
|
+
mock.patch(
|
|
826
|
+
"skmemory.setup_wizard.compose_up",
|
|
827
|
+
return_value=subprocess.CompletedProcess(
|
|
828
|
+
args=[], returncode=0, stdout="", stderr=""
|
|
829
|
+
),
|
|
830
|
+
),
|
|
831
|
+
):
|
|
832
|
+
result = runner.invoke(cli, ["setup", "start", "--service", "skvector"])
|
|
833
|
+
|
|
834
|
+
assert result.exit_code == 0
|
|
835
|
+
assert "Started" in result.output
|
|
836
|
+
|
|
837
|
+
def test_setup_stop_cli(self) -> None:
|
|
838
|
+
from skmemory.cli import cli
|
|
839
|
+
|
|
840
|
+
runner = CliRunner()
|
|
841
|
+
with (
|
|
842
|
+
mock.patch("skmemory.config.load_config", return_value=SKMemoryConfig()),
|
|
843
|
+
mock.patch(
|
|
844
|
+
"skmemory.setup_wizard.detect_platform",
|
|
845
|
+
return_value=PlatformInfo(
|
|
846
|
+
os_name="Linux", docker_available=True, compose_available=True
|
|
847
|
+
),
|
|
848
|
+
),
|
|
849
|
+
mock.patch(
|
|
850
|
+
"skmemory.setup_wizard.compose_down",
|
|
851
|
+
return_value=subprocess.CompletedProcess(
|
|
852
|
+
args=[], returncode=0, stdout="", stderr=""
|
|
853
|
+
),
|
|
854
|
+
),
|
|
855
|
+
):
|
|
856
|
+
result = runner.invoke(cli, ["setup", "stop"])
|
|
857
|
+
|
|
858
|
+
assert result.exit_code == 0
|
|
859
|
+
assert "stopped" in result.output.lower()
|
|
860
|
+
|
|
861
|
+
def test_setup_stop_individual(self) -> None:
|
|
862
|
+
from skmemory.cli import cli
|
|
863
|
+
|
|
864
|
+
runner = CliRunner()
|
|
865
|
+
with (
|
|
866
|
+
mock.patch("skmemory.config.load_config", return_value=SKMemoryConfig()),
|
|
867
|
+
mock.patch(
|
|
868
|
+
"skmemory.setup_wizard.detect_platform",
|
|
869
|
+
return_value=PlatformInfo(
|
|
870
|
+
os_name="Linux", docker_available=True, compose_available=True
|
|
871
|
+
),
|
|
872
|
+
),
|
|
873
|
+
mock.patch(
|
|
874
|
+
"subprocess.run",
|
|
875
|
+
return_value=subprocess.CompletedProcess(
|
|
876
|
+
args=[], returncode=0, stdout="", stderr=""
|
|
877
|
+
),
|
|
878
|
+
),
|
|
879
|
+
):
|
|
880
|
+
result = runner.invoke(cli, ["setup", "stop", "--service", "skvector"])
|
|
881
|
+
|
|
882
|
+
assert result.exit_code == 0
|
|
883
|
+
|
|
884
|
+
def test_setup_reset_cli(self) -> None:
|
|
885
|
+
from skmemory.cli import cli
|
|
886
|
+
|
|
887
|
+
runner = CliRunner()
|
|
888
|
+
with (
|
|
889
|
+
mock.patch("skmemory.config.load_config", return_value=SKMemoryConfig()),
|
|
890
|
+
mock.patch(
|
|
891
|
+
"skmemory.setup_wizard.detect_platform",
|
|
892
|
+
return_value=PlatformInfo(
|
|
893
|
+
os_name="Linux", docker_available=True, compose_available=True
|
|
894
|
+
),
|
|
895
|
+
),
|
|
896
|
+
mock.patch(
|
|
897
|
+
"skmemory.setup_wizard.compose_down",
|
|
898
|
+
return_value=subprocess.CompletedProcess(
|
|
899
|
+
args=[], returncode=0, stdout="", stderr=""
|
|
900
|
+
),
|
|
901
|
+
),
|
|
902
|
+
mock.patch("skmemory.config.CONFIG_PATH", Path("/tmp/nonexistent_config.yaml")),
|
|
903
|
+
):
|
|
904
|
+
result = runner.invoke(cli, ["setup", "reset", "--yes"])
|
|
905
|
+
|
|
906
|
+
assert result.exit_code == 0
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
# ═══════════════════════════════════════════════════════════
|
|
910
|
+
# _get_store integration test (config wiring)
|
|
911
|
+
# ═══════════════════════════════════════════════════════════
|
|
912
|
+
|
|
913
|
+
|
|
914
|
+
class TestGetStoreWiring:
|
|
915
|
+
"""Test that _get_store wires both skvector and skgraph from config."""
|
|
916
|
+
|
|
917
|
+
def test_get_store_wires_skgraph(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
918
|
+
from skmemory.cli import _get_store
|
|
919
|
+
|
|
920
|
+
monkeypatch.delenv("SKMEMORY_SKVECTOR_URL", raising=False)
|
|
921
|
+
monkeypatch.delenv("SKMEMORY_SKVECTOR_KEY", raising=False)
|
|
922
|
+
monkeypatch.delenv("SKMEMORY_SKGRAPH_URL", raising=False)
|
|
923
|
+
|
|
924
|
+
cfg = SKMemoryConfig(
|
|
925
|
+
skvector_url="http://localhost:6333",
|
|
926
|
+
skgraph_url="redis://localhost:6379",
|
|
927
|
+
)
|
|
928
|
+
with mock.patch("skmemory.config.load_config", return_value=cfg):
|
|
929
|
+
_get_store()
|
|
930
|
+
|
|
931
|
+
# SKVector may fail to import but that's OK — we test the wiring logic
|
|
932
|
+
# SKGraph should be wired (lazy init, won't connect yet)
|
|
933
|
+
assert True # at least no crash (store.graph/vector may be None if backends unavailable)
|
|
934
|
+
|
|
935
|
+
def test_get_store_no_config_no_env(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
936
|
+
from skmemory.cli import _get_store
|
|
937
|
+
|
|
938
|
+
monkeypatch.delenv("SKMEMORY_SKVECTOR_URL", raising=False)
|
|
939
|
+
monkeypatch.delenv("SKMEMORY_SKVECTOR_KEY", raising=False)
|
|
940
|
+
monkeypatch.delenv("SKMEMORY_SKGRAPH_URL", raising=False)
|
|
941
|
+
|
|
942
|
+
with mock.patch("skmemory.config.load_config", return_value=None):
|
|
943
|
+
store = _get_store()
|
|
944
|
+
|
|
945
|
+
assert store.vector is None
|
|
946
|
+
assert store.graph is None
|
|
947
|
+
assert store.primary is not None
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
import urllib.error # noqa: E402
|