diff --git a/memory-update.md b/memory-update.md index e2f6a6b1..49815951 100644 --- a/memory-update.md +++ b/memory-update.md @@ -1060,3 +1060,69 @@ - Updated control-center priorities: - keep Phase 5 TUI refresh completion ahead of semantic Phase A implementation - semantic index Phase A now explicit next-track deliverable after current UI slice. + +# Memory Update (2026-03-02) + +## TUI v2 Phase 5 tree navigation parity landed +- Added explicit left-profile-tree keyboard navigation in `src/seedpass/tui_v2/app.py`: + - `Up` / `Down` for tree cursor movement + - `Ctrl+O` to open/switch the selected profile +- Added palette command parity/discoverability: + - `profile-tree-next`, `profile-tree-prev`, `profile-tree-open` +- Updated left panel rendering with cursor + active profile markers and refreshed tree state after profile add/remove/rename/switch. +- Extended Textual interaction + matrix tests: + - `src/tests/test_tui_v2_textual_interactions.py` + - `src/tests/test_tui_v2_action_matrix.py` + +## Semantic index implementation started (Phase A/B baseline) +- Added local derived semantic index core: + - `src/seedpass/core/semantic_index.py` + - profile-local `semantic_index/manifest.json` + `records.json` + - token-overlap retrieval baseline, with high-risk kinds excluded by default (e.g., `seed`, `managed_account`). +- Added profile config flag and service wrappers: + - `semantic_index_enabled` default `false` + - `ConfigManager` + `ConfigService` getters/setters + - new `SemanticIndexService` in `src/seedpass/core/api.py` +- Added CLI command group: + - `seedpass semantic status|enable|disable|build|rebuild|search` +- Added API endpoints: + - `GET /api/v1/semantic/status` + - `POST /api/v1/semantic/build` + - `POST /api/v1/semantic/rebuild` + - `POST /api/v1/semantic/search` + - `POST /api/v1/semantic/config` +- Updated capabilities discovery for semantic CLI/API surfaces. + +## Tests executed and passing +- `poetry run pytest -q src/tests/test_tui_v2_textual_interactions.py src/tests/test_tui_v2_action_matrix.py src/tests/test_semantic_index.py src/tests/test_cli_semantic.py src/tests/test_api_new_endpoints.py` => `95 passed` +- Expanded target including CLI capability coverage: + - `poetry run pytest -q src/tests/test_cli_semantic.py src/tests/test_api_new_endpoints.py src/tests/test_semantic_index.py src/tests/test_tui_v2_textual_interactions.py src/tests/test_tui_v2_action_matrix.py src/tests/test_typer_cli.py` => `149 passed` + +## Docs/control-plane updates +- Updated semantic roadmap status/progress and phase checkboxes: + - `docs/semantic_vector_index_plan.md` +- Updated unified prioritization doc with current state and next queue: + - `docs/dev_control_center.md` +- Updated UI refresh plan to mark Phase 5 tree-navigation completion: + - `docs/tui_v2_ui_refresh_plan.md` + +## Committed + pushed +- Branch: `beta` +- Commit: `165e2db` +- Message: `feat: add semantic index scaffolding, cli/api surfaces, and tui tree nav polish` + +## Recommended immediate next slice +1. Implement semantic Phase C in both TUIs (status/build/search commands + visible semantic-state indicator in ribbon/settings). +2. Add incremental semantic index updates on entry/tag/link mutation paths (avoid rebuild-only behavior). +3. Add hybrid ranking mode contract (`keyword|hybrid|semantic`) across CLI/API/TUI with tests. + +## Memory Update (2026-03-02, semantic Phase C slice) +- Wired semantic controls into TUI v2 palette and top ribbon semantic state (`SEM: off/stale/ready(n)/err/n/a`). +- Added TUI v2 commands: `semantic-status`, `semantic-enable`, `semantic-disable`, `semantic-build`, `semantic-rebuild`, `semantic-search `. +- Wired semantic service bootstrap into CLI TUI launch path (`_prime_tui2_service` + `launch_tui2` factories). +- Added legacy TUI Settings option `18. Semantic Index` with submenu for status/enable/disable/build/rebuild/search. +- Added tests: + - `test_settings_menu.py` semantic submenu dispatch/search flow + - `test_tui_v2_textual_interactions.py` semantic command success/validation paths + - `test_tui_v2_action_matrix.py` semantic command matrix coverage +- Validation run: `102 passed` across targeted settings/TUI/semantic/CLI suites. diff --git a/src/main.py b/src/main.py index 7a00430c..a737f6ba 100644 --- a/src/main.py +++ b/src/main.py @@ -21,6 +21,7 @@ from utils.color_scheme import color_text import importlib from seedpass.core.manager import PasswordManager, restore_backup_index +from seedpass.core.api import SemanticIndexService from seedpass.core.errors import SeedPassError from nostr.client import NostrClient from seedpass.core.entry_types import EntryType @@ -1112,6 +1113,121 @@ def handle_nostr_menu(password_manager: PasswordManager) -> None: pause() +def handle_semantic_index_menu(password_manager: PasswordManager) -> None: + """Submenu for semantic index status/build/search controls.""" + service = SemanticIndexService(password_manager) + while True: + fp, parent_fp, child_fp = getattr( + password_manager, + "header_fingerprint_args", + (getattr(password_manager, "current_fingerprint", None), None, None), + ) + clear_header_with_notification( + password_manager, + fp, + "Main Menu > Settings > Semantic Index", + parent_fingerprint=parent_fp, + child_fingerprint=child_fp, + ) + print(color_text("\nSemantic Index:", "menu")) + print(color_text("1. Show status", "menu")) + print(color_text("2. Enable", "menu")) + print(color_text("3. Disable", "menu")) + print(color_text("4. Build", "menu")) + print(color_text("5. Rebuild", "menu")) + print(color_text("6. Search", "menu")) + choice = input("Select an option or press Enter to go back: ").strip() + try: + if choice == "1": + payload = service.status() + print( + colored( + "Status: " + f"enabled={bool(payload.get('enabled', False))}, " + f"built={bool(payload.get('built', False))}, " + f"records={int(payload.get('records', 0))}", + "green", + ) + ) + pause() + elif choice == "2": + payload = service.set_enabled(True) + print( + colored( + f"Semantic index enabled (records={int(payload.get('records', 0))}).", + "green", + ) + ) + pause() + elif choice == "3": + payload = service.set_enabled(False) + print( + colored( + f"Semantic index disabled (records={int(payload.get('records', 0))}).", + "yellow", + ) + ) + pause() + elif choice == "4": + payload = service.build() + print( + colored( + f"Semantic index built with {int(payload.get('records', 0))} records.", + "green", + ) + ) + pause() + elif choice == "5": + payload = service.rebuild() + print( + colored( + f"Semantic index rebuilt with {int(payload.get('records', 0))} records.", + "green", + ) + ) + pause() + elif choice == "6": + query = input("Enter semantic search query: ").strip() + if not query: + print(colored("Search query required.", "yellow")) + pause() + continue + results = service.search(query, k=10, kind=None) + if not results: + print(colored("No semantic matches found.", "yellow")) + pause() + continue + print(color_text("\nSemantic Matches:", "menu")) + for row in results: + entry_id = int(row.get("entry_id", 0)) + kind = str(row.get("kind", "")) + label = str(row.get("label", "")) + score = float(row.get("score", 0.0)) + print( + colored( + f"- #{entry_id} [{kind}] {label} (score={score:.3f})", + "cyan", + ) + ) + pause() + elif not choice: + break + else: + print(colored("Invalid choice.", "red")) + except PasswordPromptError as exc: + logging.warning("Semantic submenu action cancelled: %s", exc) + print(colored(f"Action cancelled: {exc}", "yellow")) + pause() + except SeedPassError as exc: + logging.error("Semantic submenu action failed: %s", exc, exc_info=True) + print(colored(f"Action failed: {exc}", "red")) + pause() + except Exception as exc: + logging.error("Semantic submenu action failed: %s", exc, exc_info=True) + print(colored(f"Semantic action failed: {exc}", "red")) + pause() + + def handle_settings(password_manager: PasswordManager) -> None: """Interactive settings menu with submenus for profiles and Nostr.""" while True: @@ -1145,6 +1261,7 @@ def handle_settings(password_manager: PasswordManager) -> None: print(color_text("15. Toggle Secret Mode", "menu")) print(color_text("16. Toggle Offline Mode (default ON)", "menu")) print(color_text("17. Toggle Quick Unlock", "menu")) + print(color_text("18. Semantic Index", "menu")) choice = input("Select an option or press Enter to go back: ").strip() try: if choice == "1": @@ -1248,6 +1365,8 @@ def handle_settings(password_manager: PasswordManager) -> None: elif choice == "17": handle_toggle_quick_unlock(password_manager) pause() + elif choice == "18": + handle_semantic_index_menu(password_manager) elif not choice: break else: diff --git a/src/seedpass/cli/__init__.py b/src/seedpass/cli/__init__.py index f783bdf0..9aaa81f6 100644 --- a/src/seedpass/cli/__init__.py +++ b/src/seedpass/cli/__init__.py @@ -96,6 +96,7 @@ def _prime_tui2_service( ProfileService, ConfigService, NostrService, + SemanticIndexService, SyncService, UtilityService, VaultService, @@ -111,6 +112,7 @@ def _prime_tui2_service( "sync": SyncService(pm) if pm is not None else None, "utility": UtilityService(pm) if pm is not None else None, "vault": VaultService(pm) if pm is not None else None, + "semantic": SemanticIndexService(pm) if pm is not None else None, }, None except Exception as exc: # pragma: no cover - defensive runtime path return None, exc @@ -163,6 +165,7 @@ def main( sync_service_factory=lambda: services["sync"], utility_service_factory=lambda: services["utility"], vault_service_factory=lambda: services["vault"], + semantic_service_factory=lambda: services["semantic"], ) if launched: return @@ -225,6 +228,7 @@ def tui2( sync_service_factory=lambda: services["sync"], utility_service_factory=lambda: services["utility"], vault_service_factory=lambda: services["vault"], + semantic_service_factory=lambda: services["semantic"], ) if launched: return diff --git a/src/seedpass/tui_v2/app.py b/src/seedpass/tui_v2/app.py index b163cbf1..6277a23c 100644 --- a/src/seedpass/tui_v2/app.py +++ b/src/seedpass/tui_v2/app.py @@ -95,6 +95,7 @@ def launch_tui2( sync_service_factory: Any | None = None, utility_service_factory: Any | None = None, vault_service_factory: Any | None = None, + semantic_service_factory: Any | None = None, app_hook: Any | None = None, ) -> bool: """Launch TUI v2 when runtime dependencies are available. @@ -343,6 +344,7 @@ def launch_tui2( "setting-secret [delay] | setting-offline | " "setting-quick-unlock | setting-timeout | " "setting-kdf-iterations | setting-kdf-mode | " + "semantic-status | semantic-enable | semantic-disable | semantic-build | semantic-rebuild | semantic-search | " "relay-list | relay-add | relay-rm | " "relay-reset | npub | nostr-reset-sync-state | nostr-fresh-namespace | " "sync-now | sync-bg | " @@ -413,6 +415,8 @@ def launch_tui2( self._sync_service = None self._utility_service = None self._vault_service = None + self._semantic_service = None + self._semantic_state = "n/a" self._selected_entry_id: int | None = None self._selected_entry: dict[str, Any] | None = None self._last_query = "" @@ -500,6 +504,16 @@ def launch_tui2( except Exception as exc: self._vault_service = None self._log_activity(f"Vault service unavailable: {exc}") + try: + self._semantic_service = ( + semantic_service_factory() + if callable(semantic_service_factory) + else None + ) + except Exception as exc: + self._semantic_service = None + self._log_activity(f"Semantic service unavailable: {exc}") + self._refresh_semantic_state() self._update_filters_panel() self._update_help_overlay() self._update_activity_panel() @@ -563,10 +577,33 @@ def launch_tui2( f" | Archive: {self.archive_scope}" f" | Session: {lock_state}" f" | Density: {self._density_mode}" + f" | SEM: {self._semantic_state}" f" | Last Sync: {self._last_sync_text}" ) self.query_one("#top-ribbon", Static).update(text) + def _refresh_semantic_state(self) -> None: + if self._semantic_service is None: + self._semantic_state = "n/a" + return + status = getattr(self._semantic_service, "status", None) + if not callable(status): + self._semantic_state = "n/a" + return + try: + payload = status() or {} + enabled = bool(payload.get("enabled", False)) + built = bool(payload.get("built", False)) + records = int(payload.get("records", 0)) + if not enabled: + self._semantic_state = "off" + elif built: + self._semantic_state = f"ready({records})" + else: + self._semantic_state = "stale" + except Exception: + self._semantic_state = "err" + def _refresh_profile_tree(self) -> None: profiles: list[str] = [] if self._profile_service is not None: @@ -668,6 +705,7 @@ def launch_tui2( "Palette commands: help, help-commands, open, search, filter, " "onboarding, quickstart, stats, session-status, lock/unlock, " "density, profile-tree-next/prev/open, " + "semantic-status/build/rebuild/search, " "archive, restore, archive-filter, edit-doc/save-doc/cancel-edit, " "link-add/link-rm/link-filter/link-next/link-prev/link-open, " "add-*, notes/tags/fields, 2fa-*, profile-*, setting-*, relay-*, " @@ -698,6 +736,7 @@ def launch_tui2( "2FA: 2fa-board | 2fa-hide | 2fa-refresh | 2fa-copy ", "Profiles: profiles-list | profile-switch | profile-add | profile-remove | profile-rename | profile-tree-next | profile-tree-prev | profile-tree-open", "Settings: setting-secret | setting-offline | setting-quick-unlock | setting-timeout | setting-kdf-iterations | setting-kdf-mode", + "Semantic: semantic-status | semantic-enable | semantic-disable | semantic-build | semantic-rebuild | semantic-search ", "Nostr: relay-list | relay-add | relay-rm | relay-reset | npub | nostr-reset-sync-state | nostr-fresh-namespace", "Sync/Utility: sync-now | sync-bg | checksum-verify | checksum-update | db-export | db-import | totp-export | parent-seed-backup", "Sessions: managed-load [entry_id] | managed-exit | session-status | lock | unlock ", @@ -3309,6 +3348,155 @@ def launch_tui2( ) return + if cmd == "semantic-status": + if self._semantic_service is None: + self._set_status("Semantic service unavailable") + return + if args: + self._set_status("Usage: semantic-status") + return + status = getattr(self._semantic_service, "status", None) + if not callable(status): + self._set_status("Semantic service does not support status") + return + try: + payload = status() or {} + self._refresh_semantic_state() + self._update_top_ribbon() + self.query_one("#entry-detail", Static).update( + "Semantic Index Status\n---------------------\n\n" + + json.dumps(payload, indent=2, sort_keys=True) + ) + self._clear_failure() + self._set_status("Displayed semantic index status") + except Exception as exc: + self._record_failure( + "semantic-status failed", + exc, + retry=lambda: self._run_palette_command(raw), + hint="Press 'x' to retry.", + ) + return + + if cmd in {"semantic-enable", "semantic-disable"}: + if self._semantic_service is None: + self._set_status("Semantic service unavailable") + return + if args: + self._set_status(f"Usage: {cmd}") + return + setter = getattr(self._semantic_service, "set_enabled", None) + if not callable(setter): + self._set_status("Semantic service does not support enable/disable") + return + try: + enabled = cmd == "semantic-enable" + payload = setter(enabled) or {} + self._refresh_semantic_state() + self._update_top_ribbon() + self._clear_failure() + self._set_status( + "Semantic index " + + ("enabled" if enabled else "disabled") + + f" (records={int(payload.get('records', 0))})" + ) + except Exception as exc: + self._record_failure( + f"{cmd} failed", + exc, + retry=lambda: self._run_palette_command(raw), + hint="Press 'x' to retry.", + ) + return + + if cmd in {"semantic-build", "semantic-rebuild"}: + if self._semantic_service is None: + self._set_status("Semantic service unavailable") + return + if args: + self._set_status(f"Usage: {cmd}") + return + runner = getattr( + self._semantic_service, + "rebuild" if cmd == "semantic-rebuild" else "build", + None, + ) + if not callable(runner): + self._set_status("Semantic service does not support build/rebuild") + return + try: + payload = runner() or {} + self._refresh_semantic_state() + self._update_top_ribbon() + self._clear_failure() + verb = "rebuilt" if cmd == "semantic-rebuild" else "built" + self._set_status( + f"Semantic index {verb} (records={int(payload.get('records', 0))})" + ) + except Exception as exc: + self._record_failure( + f"{cmd} failed", + exc, + retry=lambda: self._run_palette_command(raw), + hint="Press 'x' to retry.", + ) + return + + if cmd == "semantic-search": + if self._semantic_service is None: + self._set_status("Semantic service unavailable") + return + if not args: + self._set_status("Usage: semantic-search ") + return + searcher = getattr(self._semantic_service, "search", None) + if not callable(searcher): + self._set_status("Semantic service does not support search") + return + query = " ".join(args).strip() + if not query: + self._set_status("Usage: semantic-search ") + return + try: + results = list(searcher(query, k=10, kind=None) or []) + self._refresh_semantic_state() + self._update_top_ribbon() + lines = [ + f"Semantic Search: {query}", + "---------------------------", + "", + ] + if not results: + lines.append("No semantic matches.") + else: + for row in results[:10]: + entry_id = int(row.get("entry_id", 0)) + kind = str(row.get("kind", "")) + label = str(row.get("label", "")) + score = float(row.get("score", 0.0)) + excerpt = str(row.get("excerpt", "")) + lines.append( + f"- #{entry_id} [{kind}] {label} (score={score:.3f})" + ) + if excerpt: + short = ( + excerpt + if len(excerpt) <= 120 + else excerpt[:117] + "..." + ) + lines.append(f" {short}") + self.query_one("#entry-detail", Static).update("\n".join(lines)) + self._clear_failure() + self._set_status(f"Semantic matches: {len(results)} for '{query}'") + except Exception as exc: + self._record_failure( + "semantic-search failed", + exc, + retry=lambda: self._run_palette_command(raw), + hint="Press 'x' to retry.", + ) + return + if cmd == "relay-list": if self._nostr_service is None: self._set_status("Nostr service unavailable") diff --git a/src/tests/test_settings_menu.py b/src/tests/test_settings_menu.py index 482c7cfc..25d3ae90 100644 --- a/src/tests/test_settings_menu.py +++ b/src/tests/test_settings_menu.py @@ -358,3 +358,51 @@ def test_nostr_menu_dispatches_fresh_namespace(monkeypatch): with patch("builtins.input", side_effect=lambda *_: next(inputs)): main.handle_nostr_menu(pm) handler.assert_called_once_with(pm) + + +def test_settings_menu_dispatches_semantic_submenu(monkeypatch): + with TemporaryDirectory() as tmpdir: + tmp_path = Path(tmpdir) + pm, _, _ = setup_pm(tmp_path, monkeypatch) + inputs = iter(["18", ""]) + with patch("main.handle_semantic_index_menu") as handler: + with patch("builtins.input", side_effect=lambda *_: next(inputs)): + main.handle_settings(pm) + handler.assert_called_once_with(pm) + + +def test_semantic_submenu_search_flow(monkeypatch, capsys): + class DummySemanticService: + def __init__(self, _pm): + self.last_query = None + + def status(self): + return {"enabled": False, "built": False, "records": 0} + + def set_enabled(self, enabled: bool): + return {"enabled": bool(enabled), "records": 0} + + def build(self): + return {"enabled": True, "built": True, "records": 2} + + def rebuild(self): + return {"enabled": True, "built": True, "records": 2} + + def search(self, query: str, *, k: int = 10, kind: str | None = None): + self.last_query = query + return [{"entry_id": 1, "kind": "document", "label": "Doc", "score": 0.5}] + + with TemporaryDirectory() as tmpdir: + tmp_path = Path(tmpdir) + pm, _, _ = setup_pm(tmp_path, monkeypatch) + monkeypatch.setattr(main, "SemanticIndexService", DummySemanticService) + monkeypatch.setattr(main, "pause", lambda: None) + inputs = iter(["1", "2", "4", "6", "relay docs", ""]) + with patch("builtins.input", side_effect=lambda *_: next(inputs)): + main.handle_semantic_index_menu(pm) + out = capsys.readouterr().out + assert "Status: enabled=False, built=False, records=0" in out + assert "Semantic index enabled" in out + assert "Semantic index built with 2 records" in out + assert "Semantic Matches:" in out + assert "#1 [document] Doc" in out diff --git a/src/tests/test_tui_v2_action_matrix.py b/src/tests/test_tui_v2_action_matrix.py index 9aea85e5..5a2dee6d 100644 --- a/src/tests/test_tui_v2_action_matrix.py +++ b/src/tests/test_tui_v2_action_matrix.py @@ -560,6 +560,12 @@ async def test_tui2_matrix_actions_palette_events_and_guards() -> None: app._run_palette_command("setting-timeout 300") app._run_palette_command("setting-kdf-iterations 100000") app._run_palette_command("setting-kdf-mode pbkdf2") + app._run_palette_command("semantic-status") + app._run_palette_command("semantic-enable now") + app._run_palette_command("semantic-disable now") + app._run_palette_command("semantic-build now") + app._run_palette_command("semantic-rebuild now") + app._run_palette_command("semantic-search") app._run_palette_command("relay-list") app._run_palette_command("npub") app._run_palette_command("relay-add wss://relay.example") @@ -601,6 +607,8 @@ async def test_tui2_matrix_actions_palette_events_and_guards() -> None: app._run_palette_command("lock") app._run_palette_command("unlock hunter2") app._run_palette_command("search Doc") + app._run_palette_command("semantic-status") + app._run_palette_command("semantic-search notes") app.action_focus_left() app._run_palette_command("profile-tree-next") app._run_palette_command("profile-tree-prev") diff --git a/src/tests/test_tui_v2_textual_interactions.py b/src/tests/test_tui_v2_textual_interactions.py index cffdd2ce..29b2e38b 100644 --- a/src/tests/test_tui_v2_textual_interactions.py +++ b/src/tests/test_tui_v2_textual_interactions.py @@ -562,6 +562,49 @@ class FakeVaultService: return _Resp() +class FakeSemanticService: + def __init__(self) -> None: + self.enabled = False + self.built = False + self.records = 0 + self.last_query: str | None = None + + def status(self): + return { + "enabled": bool(self.enabled), + "built": bool(self.built), + "records": int(self.records), + } + + def set_enabled(self, enabled: bool): + self.enabled = bool(enabled) + return self.status() + + def build(self): + self.built = True + self.records = 2 + return self.status() + + def rebuild(self): + self.built = True + self.records = 2 + return self.status() + + def search(self, query: str, *, k: int = 10, kind: str | None = None): + self.last_query = str(query) + if not query.strip(): + return [] + return [ + { + "entry_id": 1, + "kind": kind or "document", + "label": "Doc", + "score": 0.7, + "excerpt": f"query={query} k={k}", + } + ] + + def _build_app( service: FakeEntryService, *, @@ -571,6 +614,7 @@ def _build_app( sync_service=None, utility_service=None, vault_service=None, + semantic_service=None, ): holder: dict[str, object] = {} @@ -597,6 +641,9 @@ def _build_app( vault_service_factory=( (lambda: vault_service) if vault_service is not None else None ), + semantic_service_factory=( + (lambda: semantic_service) if semantic_service is not None else None + ), app_hook=_hook, ) assert launched is True @@ -1547,6 +1594,7 @@ async def test_tui2_textual_profiles_and_settings_palette_commands() -> None: sync = FakeSyncService() utility = FakeUtilityService() vault = FakeVaultService() + semantic = FakeSemanticService() app = _build_app( service, profile_service=profiles, @@ -1555,6 +1603,7 @@ async def test_tui2_textual_profiles_and_settings_palette_commands() -> None: sync_service=sync, utility_service=utility, vault_service=vault, + semantic_service=semantic, ) async with app.run_test() as pilot: @@ -1611,6 +1660,34 @@ async def test_tui2_textual_profiles_and_settings_palette_commands() -> None: assert config.kdf_mode == "argon2" assert "KDF mode set to argon2" in _status_text(app) + app._run_palette_command("semantic-status") + await pilot.pause() + assert "Displayed semantic index status" in _status_text(app) + + app._run_palette_command("semantic-enable") + await pilot.pause() + assert semantic.enabled is True + assert "Semantic index enabled" in _status_text(app) + + app._run_palette_command("semantic-build") + await pilot.pause() + assert semantic.built is True + assert "Semantic index built" in _status_text(app) + + app._run_palette_command("semantic-search relay recovery") + await pilot.pause() + assert semantic.last_query == "relay recovery" + assert "Semantic matches: 1 for 'relay recovery'" in _status_text(app) + + app._run_palette_command("semantic-rebuild") + await pilot.pause() + assert "Semantic index rebuilt" in _status_text(app) + + app._run_palette_command("semantic-disable") + await pilot.pause() + assert semantic.enabled is False + assert "Semantic index disabled" in _status_text(app) + app._run_palette_command("profile-add") await pilot.pause() assert "Created profile fp-new-1" in _status_text(app) @@ -1763,6 +1840,10 @@ async def test_tui2_textual_profiles_and_settings_palette_validation() -> None: await pilot.pause() assert "Utility service unavailable" in _status_text(app) + app._run_palette_command("semantic-status") + await pilot.pause() + assert "Semantic service unavailable" in _status_text(app) + app._run_palette_command("db-export /tmp/x") await pilot.pause() assert "Vault service unavailable" in _status_text(app) @@ -1831,6 +1912,14 @@ async def test_tui2_textual_profiles_and_settings_palette_validation() -> None: await pilot.pause() assert "Nostr service unavailable" in _status_text(app) + app._run_palette_command("semantic-enable now") + await pilot.pause() + assert "Semantic service unavailable" in _status_text(app) + + app._run_palette_command("semantic-search") + await pilot.pause() + assert "Semantic service unavailable" in _status_text(app) + app._run_palette_command("npub") await pilot.pause() assert "Nostr service unavailable" in _status_text(app) @@ -1855,6 +1944,7 @@ async def test_tui2_textual_profiles_and_settings_palette_validation() -> None: vault = FakeVaultService() nostr = FakeNostrService(["wss://r1"]) sync = FakeSyncService() + semantic = FakeSemanticService() app = _build_app( service, profile_service=profiles, @@ -1863,6 +1953,7 @@ async def test_tui2_textual_profiles_and_settings_palette_validation() -> None: vault_service=vault, nostr_service=nostr, sync_service=sync, + semantic_service=semantic, ) async with app.run_test() as pilot: await pilot.pause() @@ -1879,6 +1970,14 @@ async def test_tui2_textual_profiles_and_settings_palette_validation() -> None: await pilot.pause() assert "Usage: setting-kdf-mode " in _status_text(app) + app._run_palette_command("semantic-enable now") + await pilot.pause() + assert "Usage: semantic-enable" in _status_text(app) + + app._run_palette_command("semantic-search") + await pilot.pause() + assert "Usage: semantic-search " in _status_text(app) + app._run_palette_command("checksum-update nope") await pilot.pause() assert "Usage: checksum-update" in _status_text(app)