mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-10-27 07:52:03 +00:00
feat: wire semantic controls into legacy and tui v2 interfaces
This commit is contained in:
1 parent
165e2db204
commit
ec407c611a
7 files changed
+532
No files matched your search
@@ -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 <query>`.
|
||||
- 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.
|
||||
+119
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <on|off> [delay] | setting-offline <on|off> | "
|
||||
"setting-quick-unlock <on|off> | setting-timeout <seconds> | "
|
||||
"setting-kdf-iterations <n> | setting-kdf-mode <mode> | "
|
||||
"semantic-status | semantic-enable | semantic-disable | semantic-build | semantic-rebuild | semantic-search <query> | "
|
||||
"relay-list | relay-add <url> | relay-rm <index> | "
|
||||
"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 <entry_id>",
|
||||
"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 <query>",
|
||||
"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 <password>",
|
||||
@@ -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 <query>")
|
||||
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 <query>")
|
||||
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")
|
||||
|
||||
@@ -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
|
||||
@@ -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")
|
||||
|
||||
@@ -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 <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 <query>" in _status_text(app)
|
||||
|
||||
app._run_palette_command("checksum-update nope")
|
||||
await pilot.pause()
|
||||
assert "Usage: checksum-update" in _status_text(app)
|
||||
|
||||
Reference in new issue
Block a user