mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-11-02 02:42:04 +00:00
tui2: add onboarding flow and session parity affordances
This commit is contained in:
1 parent
5c524b2086
commit
8f7075c39b
6 files changed
+326
-29
No files matched your search
@@ -66,7 +66,7 @@ Use these docs as inputs, but treat this file as the decision layer:
|
||||
|
||||
1. Continue feature-development parity polish in TUI v2 with user-driven UX gaps.
|
||||
: prioritized gaps from side-by-side legacy vs v2 run:
|
||||
: guided first-run onboarding, stats/ops visibility, explicit lock/session affordances.
|
||||
: onboarding + stats/ops + lock/session affordances now landed; continue bug-driven UX polish.
|
||||
2. Raise TUI v2 critical-path coverage beyond current gate floor (target >82% for `src/seedpass/tui_v2/app.py`).
|
||||
3. Advance Nostr resilience validation with optional real-relay soak lane after deterministic suites.
|
||||
4. Execute a tagged release integrity run and attach evidence URLs in readiness checklist.
|
||||
@@ -128,6 +128,11 @@ Progress update (2026-03-02):
|
||||
- added full palette command reference (`help-commands`, alias `commands`)
|
||||
- `help` now also renders in-app command reference in entry detail pane
|
||||
- focused TUI gate remains green with improved app coverage (`79.97%`)
|
||||
- Session affordance parity improved in TUI v2:
|
||||
- added `session-status`, `lock`, and `unlock <password>` palette commands
|
||||
- left pane now displays explicit session lock state and managed-session indicator
|
||||
- lock state blocks open/reveal/QR workflows until unlock succeeds
|
||||
- interaction + matrix tests extended for usage, success, and failure paths
|
||||
|
||||
## 6) Working Rule
|
||||
|
||||
|
||||
@@ -51,19 +51,17 @@ Purpose: map legacy interactive workflows to Textual TUI v2 equivalents and isol
|
||||
| Export TOTP codes | Settings | `totp-export` | Implemented | |
|
||||
| Backup/reveal parent seed | Settings | `parent-seed-backup [path] [password]` | Implemented | |
|
||||
| Stats view | Settings > Stats | `stats` (palette) | Implemented | In-app dashboard in detail pane. |
|
||||
| First-run onboarding guidance | Startup path/menu guidance | Empty-vault onboarding panel + `onboarding` (alias: `welcome`) + `quickstart` | Implemented | Guided steps for first entry, inspect/reveal, and operations. |
|
||||
| Display npub | Main utility flow | `npub` (alias: `nostr-pubkey`) | Implemented | Displays active profile npub and QR payload in the sensitive panel. |
|
||||
| View archived-only list | Settings/List flow | `h` cycle archive scope or `archive-filter <active|all|archived>` | Implemented | Supports active-only/default, all, and archived-only views. |
|
||||
| Reset Nostr sync state | Settings > Nostr | `nostr-reset-sync-state` | Implemented | Added to TUI v2 palette + tests. |
|
||||
| Start fresh Nostr namespace | Settings > Nostr | `nostr-fresh-namespace` | Implemented | Added to TUI v2 palette + tests. |
|
||||
| Load managed-account session | Retrieve managed account action | `managed-load (optional: entry_id)` | Implemented | Added in TUI v2 palette + tests. |
|
||||
| Exit managed-account session | Main loop when in child profile | `managed-exit` | Implemented | Added in TUI v2 palette + tests. |
|
||||
| Session visibility | Operational/session affordance | `session-status` + left-pane session lines | Implemented | Shows lock state + managed session state. |
|
||||
| Lock vault | Operational/session affordance | `lock` | Implemented | Locks session, clears selected view, blocks reveal/open actions. |
|
||||
| Unlock vault | Operational/session affordance | `unlock <password>` | Implemented | Restores unlocked state; usage + failure paths covered in tests. |
|
||||
|
||||
## Prioritized Gap Queue
|
||||
|
||||
1. Medium: guided first-run onboarding parity in TUI v2 (legacy has explicit startup path).
|
||||
2. Medium: explicit lock/session affordances and status visibility parity in TUI v2.
|
||||
|
||||
## Suggested implementation order
|
||||
|
||||
1. Land guided onboarding panel/flow parity improvements.
|
||||
2. Add lock/session visibility commands and test coverage.
|
||||
No active medium/high parity gaps are currently identified in this matrix.
|
||||
@@ -825,3 +825,62 @@
|
||||
- `docs/dev_control_center.md`
|
||||
- `docs/tui_v2_legacy_parity_matrix.md`
|
||||
(remaining medium gap: explicit lock/session affordances).
|
||||
|
||||
## 2026-03-02 Parity UX slice: explicit session lock/unlock/status affordances
|
||||
- Implemented in `src/seedpass/tui_v2/app.py`:
|
||||
- new palette commands:
|
||||
- `session-status`
|
||||
- `lock`
|
||||
- `unlock <password>`
|
||||
- left panel now displays:
|
||||
- `Session: locked|unlocked`
|
||||
- `Managed: #<entry_id>|(none)`
|
||||
- managed session visibility wired:
|
||||
- `managed-load` sets active managed-session marker
|
||||
- `managed-exit` clears active managed-session marker
|
||||
- lock guard behavior:
|
||||
- `open`, `reveal`, and `qr` actions are blocked while locked
|
||||
- selected entry/view state is cleared on lock
|
||||
- unlock restores normal browse/reveal behavior
|
||||
- Command discoverability updates:
|
||||
- palette placeholder/help summary/reference now include session commands.
|
||||
- Tests added/updated:
|
||||
- `src/tests/test_tui_v2_textual_interactions.py`
|
||||
- `test_tui2_textual_palette_session_status_lock_and_unlock`
|
||||
- managed session test now asserts filter-panel state line updates
|
||||
- validation suite now covers `session-status`, `lock`, and `unlock` usage/unavailable paths
|
||||
- `src/tests/test_tui_v2_action_matrix.py`
|
||||
- matrix command paths now exercise session command usage + runtime cases
|
||||
- Validation:
|
||||
- `poetry run pytest -q src/tests/test_tui_v2_textual_interactions.py src/tests/test_tui_v2_action_matrix.py`
|
||||
- Result: `28 passed`.
|
||||
- Planning docs updated:
|
||||
- `docs/tui_v2_legacy_parity_matrix.md` now marks session lock/unlock/status rows as implemented.
|
||||
- `docs/dev_control_center.md` priority list now reflects onboarding as the primary remaining UX parity item.
|
||||
|
||||
## 2026-03-02 Parity UX slice: first-run onboarding flow parity
|
||||
- Implemented explicit onboarding guidance in `src/seedpass/tui_v2/app.py`:
|
||||
- new palette command: `onboarding`
|
||||
- alias: `welcome`
|
||||
- `quickstart` remains supported and now renders the same onboarding guide for backward compatibility.
|
||||
- Empty-vault startup behavior now renders the onboarding panel and status guidance:
|
||||
- status text updated to: run `onboarding` (or `quickstart`) to begin.
|
||||
- Onboarding guide content now provides a structured first-run path:
|
||||
- create first entry
|
||||
- inspect/reveal/QR verification
|
||||
- operate with stats/help/linking commands.
|
||||
- Command discoverability updates:
|
||||
- palette placeholder, help summary, and reference include onboarding.
|
||||
- left action hints now show onboarding/quickstart/stats path.
|
||||
- Tests updated:
|
||||
- `src/tests/test_tui_v2_textual_interactions.py`
|
||||
- quickstart/stats test now includes onboarding command assertions
|
||||
- empty-vault test now validates onboarding panel rendering
|
||||
- `src/tests/test_tui_v2_action_matrix.py`
|
||||
- matrix coverage now includes onboarding usage and success paths
|
||||
- Validation:
|
||||
- `poetry run pytest -q src/tests/test_tui_v2_textual_interactions.py src/tests/test_tui_v2_action_matrix.py`
|
||||
- Result: `28 passed`.
|
||||
- Planning docs updated:
|
||||
- `docs/tui_v2_legacy_parity_matrix.md` now marks onboarding parity as implemented and clears active medium/high parity gaps.
|
||||
- `docs/dev_control_center.md` now reflects that the tracked onboarding/stats/session UX gaps are landed.
|
||||
+156
-19
@@ -282,7 +282,7 @@ def launch_tui2(
|
||||
yield Input(
|
||||
placeholder=(
|
||||
"Command palette: help | open <id> | search <q> | "
|
||||
"help-commands | quickstart | stats | "
|
||||
"help-commands | onboarding | quickstart | stats | session-status | lock | unlock <password> | "
|
||||
"filter <kind> | archive | restore | "
|
||||
"archive-filter <active|all|archived> | "
|
||||
"add-password <label> <length> [username] [url] | "
|
||||
@@ -306,7 +306,7 @@ def launch_tui2(
|
||||
"sync-now | sync-bg | "
|
||||
"checksum-verify | checksum-update | db-export <path> | db-import <path> | "
|
||||
"totp-export <path> | parent-seed-backup [path] [password] | "
|
||||
"managed-load [entry_id] | managed-exit | "
|
||||
"managed-load [entry_id] | managed-exit | session-status | lock | unlock <password> | "
|
||||
"doc-export [output_path] | "
|
||||
"link-add <target> [relation] [note] | "
|
||||
"link-rm <target> [relation] | "
|
||||
@@ -382,6 +382,8 @@ def launch_tui2(
|
||||
self._last_status_message = ""
|
||||
self._time_now = time.time
|
||||
self._totp_rows: list[dict[str, Any]] = []
|
||||
self._session_locked = False
|
||||
self._managed_session_entry_id: int | None = None
|
||||
self._totp_tick = self.set_interval(1.0, self._tick_totp_board)
|
||||
try:
|
||||
self._service = (
|
||||
@@ -491,32 +493,41 @@ def launch_tui2(
|
||||
self.query_one("#secret-detail", Static).update(text)
|
||||
|
||||
def _quickstart_text(self) -> str:
|
||||
return self._onboarding_text()
|
||||
|
||||
def _onboarding_text(self) -> str:
|
||||
return "\n".join(
|
||||
[
|
||||
"Quick Start",
|
||||
"-----------",
|
||||
"Onboarding Quick Start",
|
||||
"----------------------",
|
||||
"",
|
||||
"Welcome to SeedPass TUI v2.",
|
||||
"Your vault currently has no active entries.",
|
||||
"",
|
||||
"Try these first actions in the palette (Ctrl+P):",
|
||||
'- add-password "Site" 16 user https://site.example',
|
||||
'- add-totp "Authenticator" 30 6',
|
||||
'- add-document "Runbook" md "starter notes"',
|
||||
"Step 1: Create your first entry (Ctrl+P then run one command):",
|
||||
'- add-password "Site" 16 user https://site.example (login)',
|
||||
'- add-totp "Authenticator" 30 6 (2FA)',
|
||||
'- add-document "Runbook" md "starter notes" (KB)',
|
||||
"",
|
||||
"Step 2: Inspect and verify",
|
||||
"- open <id> to inspect an entry",
|
||||
"- v to reveal a selected secret, g to render QR when supported",
|
||||
"",
|
||||
"Step 3: Operate and scale",
|
||||
"- stats for vault overview",
|
||||
"- help-commands for full command reference",
|
||||
"- link-add / tag-add / notes-set to build knowledge graph context",
|
||||
"",
|
||||
"Useful navigation:",
|
||||
"- / search, j jump, f kind filter, h archive scope",
|
||||
"- 1/2/3 focus panes, ? keyboard help",
|
||||
"",
|
||||
"Operational shortcuts:",
|
||||
"- stats (vault summary)",
|
||||
"- help-commands (full palette reference)",
|
||||
]
|
||||
)
|
||||
|
||||
def _palette_help_summary(self) -> str:
|
||||
return (
|
||||
"Palette commands: help, help-commands, open, search, filter, "
|
||||
"quickstart, stats, "
|
||||
"onboarding, quickstart, stats, session-status, lock/unlock, "
|
||||
"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-*, "
|
||||
@@ -532,7 +543,7 @@ def launch_tui2(
|
||||
"-----------------",
|
||||
"",
|
||||
"Core: help | help-commands | open <id> | jump <id> | search <q>",
|
||||
"Core Ops: quickstart | stats",
|
||||
"Core Ops: onboarding | quickstart | stats | session-status | lock | unlock <password>",
|
||||
"Filters: filter <kind|all> | archive-filter <active|all|archived>",
|
||||
"Pages: page-next | page-prev | page <n>",
|
||||
"",
|
||||
@@ -549,7 +560,7 @@ def launch_tui2(
|
||||
"Settings: setting-secret | setting-offline | setting-quick-unlock | setting-timeout | setting-kdf-iterations | setting-kdf-mode",
|
||||
"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",
|
||||
"Sessions: managed-load [entry_id] | managed-exit | session-status | lock | unlock <password>",
|
||||
"",
|
||||
"Tip: use '?' for compact keyboard help overlay.",
|
||||
]
|
||||
@@ -661,6 +672,12 @@ def launch_tui2(
|
||||
if fingerprint
|
||||
else "Fingerprint: (default)"
|
||||
)
|
||||
session_state = "locked" if self._session_locked else "unlocked"
|
||||
managed_state = (
|
||||
f"#{self._managed_session_entry_id}"
|
||||
if self._managed_session_entry_id is not None
|
||||
else "(none)"
|
||||
)
|
||||
text = "\n".join(
|
||||
[
|
||||
"SeedPass ◈ TUI v2",
|
||||
@@ -670,6 +687,8 @@ def launch_tui2(
|
||||
f"Filter : {self.filter_kind}",
|
||||
f"Archive: {self.archive_scope}",
|
||||
f"Links : {self.link_relation_filter}",
|
||||
f"Session: {session_state}",
|
||||
f"Managed: {managed_state}",
|
||||
(
|
||||
f"Results: {len(self._all_results)} | "
|
||||
f"Page: {self._result_page + 1}/{self._total_pages()}"
|
||||
@@ -692,7 +711,7 @@ def launch_tui2(
|
||||
"- Ctrl+S save doc",
|
||||
"- Ctrl+P command palette",
|
||||
"- add-* create entries",
|
||||
"- quickstart/stats via palette",
|
||||
"- onboarding/quickstart/stats via palette",
|
||||
"- tags/notes/fields via palette",
|
||||
"- docs: edit/save/export",
|
||||
"- 6 toggle dedicated 2FA board",
|
||||
@@ -700,6 +719,7 @@ def launch_tui2(
|
||||
"- npub + nostr reset/fresh namespace",
|
||||
"- checksum/db/totp/seed backup",
|
||||
"- managed-load/managed-exit session",
|
||||
"- session-status, lock, unlock",
|
||||
"- l cycle link relation",
|
||||
"- [ / ] select link",
|
||||
"- o open link target",
|
||||
@@ -820,7 +840,7 @@ def launch_tui2(
|
||||
)
|
||||
if is_empty_vault:
|
||||
self.query_one("#entry-detail", Static).update(
|
||||
self._quickstart_text()
|
||||
self._onboarding_text()
|
||||
)
|
||||
else:
|
||||
self.query_one("#entry-detail", Static).update("No entries match.")
|
||||
@@ -834,7 +854,7 @@ def launch_tui2(
|
||||
self._current_link_cursor = 0
|
||||
if is_empty_vault:
|
||||
self._set_status(
|
||||
"Vault is empty. Run 'quickstart' or add your first entry."
|
||||
"Vault is empty. Run 'onboarding' (or 'quickstart') to begin."
|
||||
)
|
||||
else:
|
||||
self._set_status("No entries match current filter/search")
|
||||
@@ -931,6 +951,9 @@ def launch_tui2(
|
||||
self._render_current_page(preserve_selected=not reset_page)
|
||||
|
||||
def _show_entry(self, entry_index: int) -> None:
|
||||
if self._session_locked:
|
||||
self._set_status("Vault is locked. Run: unlock <password>")
|
||||
return
|
||||
if self._service is None:
|
||||
return
|
||||
try:
|
||||
@@ -1499,6 +1522,8 @@ def launch_tui2(
|
||||
self._refresh_doc_edit_help()
|
||||
|
||||
def _selected_entry_payload(self) -> dict[str, Any]:
|
||||
if self._session_locked:
|
||||
raise ValueError("Vault is locked. Run: unlock <password>")
|
||||
if self._service is None:
|
||||
raise ValueError("Entry service unavailable")
|
||||
if self._selected_entry_id is None:
|
||||
@@ -1604,11 +1629,18 @@ def launch_tui2(
|
||||
self._set_status(f"Applied search: {query}")
|
||||
return
|
||||
|
||||
if cmd in {"onboarding", "welcome"}:
|
||||
if args:
|
||||
self._set_status("Usage: onboarding")
|
||||
return
|
||||
self.query_one("#entry-detail", Static).update(self._onboarding_text())
|
||||
self._set_status("Displayed onboarding guide")
|
||||
return
|
||||
if cmd == "quickstart":
|
||||
if args:
|
||||
self._set_status("Usage: quickstart")
|
||||
return
|
||||
self.query_one("#entry-detail", Static).update(self._quickstart_text())
|
||||
self.query_one("#entry-detail", Static).update(self._onboarding_text())
|
||||
self._set_status("Displayed quick start guide")
|
||||
return
|
||||
|
||||
@@ -1619,6 +1651,103 @@ def launch_tui2(
|
||||
self.query_one("#entry-detail", Static).update(self._stats_text())
|
||||
self._set_status("Displayed vault stats")
|
||||
return
|
||||
if cmd == "session-status":
|
||||
if args:
|
||||
self._set_status("Usage: session-status")
|
||||
return
|
||||
lock_state = "locked" if self._session_locked else "unlocked"
|
||||
managed_state = (
|
||||
f"entry #{self._managed_session_entry_id}"
|
||||
if self._managed_session_entry_id is not None
|
||||
else "none"
|
||||
)
|
||||
vault_state = (
|
||||
"connected" if self._vault_service is not None else "unavailable"
|
||||
)
|
||||
self.query_one("#entry-detail", Static).update(
|
||||
"\n".join(
|
||||
[
|
||||
"Session Status",
|
||||
"--------------",
|
||||
"",
|
||||
f"Vault lock state: {lock_state}",
|
||||
f"Managed account session: {managed_state}",
|
||||
f"Vault service: {vault_state}",
|
||||
]
|
||||
)
|
||||
)
|
||||
self._update_filters_panel()
|
||||
self._set_status("Displayed session status")
|
||||
return
|
||||
if cmd == "lock":
|
||||
if args:
|
||||
self._set_status("Usage: lock")
|
||||
return
|
||||
if self._vault_service is None:
|
||||
self._set_status("Vault service unavailable")
|
||||
return
|
||||
locker = getattr(self._vault_service, "lock", None)
|
||||
if not callable(locker):
|
||||
self._set_status("Vault service does not support lock")
|
||||
return
|
||||
try:
|
||||
locker()
|
||||
self._session_locked = True
|
||||
self._clear_failure()
|
||||
self._selected_entry_id = None
|
||||
self._selected_entry = None
|
||||
self._current_links = []
|
||||
self._current_link_cursor = 0
|
||||
self._all_results = []
|
||||
self._result_page = 0
|
||||
self._render_current_page(preserve_selected=False)
|
||||
self._set_secret_panel(
|
||||
"Vault locked. Sensitive data hidden.\nRun: unlock <password>"
|
||||
)
|
||||
self._update_filters_panel()
|
||||
self._set_status("Vault locked")
|
||||
except Exception as exc:
|
||||
self._record_failure(
|
||||
"lock failed",
|
||||
exc,
|
||||
retry=lambda: self._run_palette_command(raw),
|
||||
hint="Press 'x' to retry.",
|
||||
)
|
||||
return
|
||||
if cmd == "unlock":
|
||||
if len(args) != 1:
|
||||
self._set_status("Usage: unlock <password>")
|
||||
return
|
||||
if self._vault_service is None:
|
||||
self._set_status("Vault service unavailable")
|
||||
return
|
||||
unlocker = getattr(self._vault_service, "unlock", None)
|
||||
if not callable(unlocker):
|
||||
self._set_status("Vault service does not support unlock")
|
||||
return
|
||||
try:
|
||||
from seedpass.core.api import UnlockRequest
|
||||
|
||||
response = unlocker(UnlockRequest(password=args[0]))
|
||||
self._session_locked = False
|
||||
self._load_entries(self._last_query, reset_page=False)
|
||||
self._set_secret_panel(
|
||||
"Sensitive data hidden. Use 'v' to reveal 🔑 or 'g' for QR ▦."
|
||||
)
|
||||
self._clear_failure()
|
||||
duration = getattr(response, "duration", None)
|
||||
if isinstance(duration, (int, float)):
|
||||
self._set_status(f"Vault unlocked in {float(duration):.2f}s")
|
||||
else:
|
||||
self._set_status("Vault unlocked")
|
||||
except Exception as exc:
|
||||
self._record_failure(
|
||||
"unlock failed",
|
||||
exc,
|
||||
retry=lambda: self._run_palette_command(raw),
|
||||
hint="Press 'x' to retry.",
|
||||
)
|
||||
return
|
||||
|
||||
if cmd == "filter":
|
||||
if len(args) != 1:
|
||||
@@ -3155,6 +3284,7 @@ def launch_tui2(
|
||||
return
|
||||
try:
|
||||
loader(entry_id)
|
||||
self._managed_session_entry_id = int(entry_id)
|
||||
self._load_entries(query="", reset_page=True)
|
||||
self._clear_failure()
|
||||
self._set_status(
|
||||
@@ -3184,6 +3314,7 @@ def launch_tui2(
|
||||
return
|
||||
try:
|
||||
exiter()
|
||||
self._managed_session_entry_id = None
|
||||
self._load_entries(query="", reset_page=True)
|
||||
self._clear_failure()
|
||||
self._set_status("Exited managed account session")
|
||||
@@ -3544,9 +3675,15 @@ def launch_tui2(
|
||||
self._set_status("Palette closed")
|
||||
|
||||
def action_reveal_selected(self, confirm: bool = False) -> None:
|
||||
if self._session_locked:
|
||||
self._set_status("Vault is locked. Run: unlock <password>")
|
||||
return
|
||||
self._show_sensitive_panel(include_qr=False, confirm=confirm)
|
||||
|
||||
def action_show_qr(self, mode: str = "default", confirm: bool = False) -> None:
|
||||
if self._session_locked:
|
||||
self._set_status("Vault is locked. Run: unlock <password>")
|
||||
return
|
||||
self._show_sensitive_panel(include_qr=True, qr_mode=mode, confirm=confirm)
|
||||
|
||||
def action_toggle_totp_board(self) -> None:
|
||||
|
||||
@@ -505,6 +505,7 @@ async def test_tui2_matrix_actions_palette_events_and_guards() -> None:
|
||||
app._run_palette_command("page abc")
|
||||
app._run_palette_command("page 0")
|
||||
app._run_palette_command("help-commands now")
|
||||
app._run_palette_command("onboarding now")
|
||||
app._run_palette_command("quickstart now")
|
||||
app._run_palette_command("stats now")
|
||||
app._run_palette_command("link-filter")
|
||||
@@ -572,6 +573,9 @@ async def test_tui2_matrix_actions_palette_events_and_guards() -> None:
|
||||
app._run_palette_command("managed-load 1 2")
|
||||
app._run_palette_command("managed-load nope")
|
||||
app._run_palette_command("managed-exit now")
|
||||
app._run_palette_command("session-status now")
|
||||
app._run_palette_command("lock now")
|
||||
app._run_palette_command("unlock")
|
||||
app._run_palette_command("doc-export one two")
|
||||
app._run_palette_command("filter")
|
||||
app._run_palette_command("archive-filter")
|
||||
@@ -581,8 +585,12 @@ async def test_tui2_matrix_actions_palette_events_and_guards() -> None:
|
||||
|
||||
app._run_palette_command("help")
|
||||
app._run_palette_command("help-commands")
|
||||
app._run_palette_command("onboarding")
|
||||
app._run_palette_command("quickstart")
|
||||
app._run_palette_command("stats")
|
||||
app._run_palette_command("session-status")
|
||||
app._run_palette_command("lock")
|
||||
app._run_palette_command("unlock hunter2")
|
||||
app._run_palette_command("search Doc")
|
||||
app._run_palette_command("filter document")
|
||||
app._run_palette_command("archive-filter all")
|
||||
|
||||
@@ -522,6 +522,9 @@ class FakeVaultService:
|
||||
self.exported: list[Path] = []
|
||||
self.imported: list[Path] = []
|
||||
self.parent_seed_backups: list[tuple[Path | None, str | None]] = []
|
||||
self.lock_calls = 0
|
||||
self.unlock_passwords: list[str] = []
|
||||
self.locked = False
|
||||
|
||||
def export_vault(self, req) -> None:
|
||||
path = Path(getattr(req, "path"))
|
||||
@@ -542,6 +545,22 @@ class FakeVaultService:
|
||||
path.write_text("seed-backup", encoding="utf-8")
|
||||
self.parent_seed_backups.append((path, password))
|
||||
|
||||
def lock(self) -> None:
|
||||
self.lock_calls += 1
|
||||
self.locked = True
|
||||
|
||||
def unlock(self, req):
|
||||
password = str(getattr(req, "password", ""))
|
||||
self.unlock_passwords.append(password)
|
||||
if password != "hunter2":
|
||||
raise ValueError("invalid password")
|
||||
self.locked = False
|
||||
|
||||
class _Resp:
|
||||
duration = 0.42
|
||||
|
||||
return _Resp()
|
||||
|
||||
|
||||
def _build_app(
|
||||
service: FakeEntryService,
|
||||
@@ -773,9 +792,15 @@ async def test_tui2_textual_quickstart_and_stats_commands() -> None:
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
|
||||
await _run_palette(app, pilot, "onboarding")
|
||||
detail = _widget_text(app, "#entry-detail")
|
||||
assert "Onboarding Quick Start" in detail
|
||||
assert "Step 1: Create your first entry" in detail
|
||||
assert "Displayed onboarding guide" in _status_text(app)
|
||||
|
||||
await _run_palette(app, pilot, "quickstart")
|
||||
detail = _widget_text(app, "#entry-detail")
|
||||
assert "Quick Start" in detail
|
||||
assert "Onboarding Quick Start" in detail
|
||||
assert "add-password" in detail
|
||||
assert "Displayed quick start guide" in _status_text(app)
|
||||
|
||||
@@ -794,7 +819,7 @@ async def test_tui2_textual_empty_vault_shows_quickstart() -> None:
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
detail = _widget_text(app, "#entry-detail")
|
||||
assert "Quick Start" in detail
|
||||
assert "Onboarding Quick Start" in detail
|
||||
assert "add-password" in detail
|
||||
assert "Vault is empty." in _status_text(app)
|
||||
|
||||
@@ -1571,11 +1596,13 @@ async def test_tui2_textual_managed_account_session_palette_commands() -> None:
|
||||
await pilot.pause()
|
||||
assert "Loaded managed account session from entry #1" in _status_text(app)
|
||||
assert service.managed_load_calls == [1]
|
||||
assert "Managed: #1" in _filters_text(app)
|
||||
|
||||
app._run_palette_command("managed-exit")
|
||||
await pilot.pause()
|
||||
assert "Exited managed account session" in _status_text(app)
|
||||
assert service.managed_exit_calls == 1
|
||||
assert "Managed: (none)" in _filters_text(app)
|
||||
|
||||
app._run_palette_command("open 2")
|
||||
app._run_palette_command("managed-load")
|
||||
@@ -1614,6 +1641,14 @@ async def test_tui2_textual_profiles_and_settings_palette_validation() -> None:
|
||||
await pilot.pause()
|
||||
assert "Vault service unavailable" in _status_text(app)
|
||||
|
||||
app._run_palette_command("lock")
|
||||
await pilot.pause()
|
||||
assert "Vault service unavailable" in _status_text(app)
|
||||
|
||||
app._run_palette_command("unlock secret")
|
||||
await pilot.pause()
|
||||
assert "Vault service unavailable" in _status_text(app)
|
||||
|
||||
app._run_palette_command("totp-export")
|
||||
await pilot.pause()
|
||||
assert "Usage: totp-export <path>" in _status_text(app)
|
||||
@@ -1744,3 +1779,58 @@ async def test_tui2_textual_profiles_and_settings_palette_validation() -> None:
|
||||
app._run_palette_command("managed-exit now")
|
||||
await pilot.pause()
|
||||
assert "Usage: managed-exit" in _status_text(app)
|
||||
|
||||
app._run_palette_command("session-status now")
|
||||
await pilot.pause()
|
||||
assert "Usage: session-status" in _status_text(app)
|
||||
|
||||
app._run_palette_command("lock now")
|
||||
await pilot.pause()
|
||||
assert "Usage: lock" in _status_text(app)
|
||||
|
||||
app._run_palette_command("unlock")
|
||||
await pilot.pause()
|
||||
assert "Usage: unlock <password>" in _status_text(app)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_tui2_textual_palette_session_status_lock_and_unlock() -> None:
|
||||
service = FakeEntryService(
|
||||
[{"id": 1, "kind": "password", "label": "Demo", "username": "a"}]
|
||||
)
|
||||
vault = FakeVaultService()
|
||||
app = _build_app(service, vault_service=vault)
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
|
||||
app._run_palette_command("session-status")
|
||||
await pilot.pause()
|
||||
assert "Displayed session status" in _status_text(app)
|
||||
assert "Vault lock state: unlocked" in _widget_text(app, "#entry-detail")
|
||||
assert "Session: unlocked" in _filters_text(app)
|
||||
|
||||
app._run_palette_command("lock")
|
||||
await pilot.pause()
|
||||
assert "Vault locked" in _status_text(app)
|
||||
assert vault.lock_calls == 1
|
||||
assert "Session: locked" in _filters_text(app)
|
||||
|
||||
app._run_palette_command("open 1")
|
||||
await pilot.pause()
|
||||
assert "Vault is locked. Run: unlock <password>" in _status_text(app)
|
||||
|
||||
app._run_palette_command("reveal")
|
||||
await pilot.pause()
|
||||
assert "Vault is locked. Run: unlock <password>" in _status_text(app)
|
||||
|
||||
app._run_palette_command("unlock wrong")
|
||||
await pilot.pause()
|
||||
assert "unlock failed: invalid password" in _status_text(app)
|
||||
assert vault.unlock_passwords[-1] == "wrong"
|
||||
|
||||
app._run_palette_command("unlock hunter2")
|
||||
await pilot.pause()
|
||||
assert "Vault unlocked in 0.42s" in _status_text(app)
|
||||
assert vault.unlock_passwords[-1] == "hunter2"
|
||||
assert "Session: unlocked" in _filters_text(app)
|
||||
Reference in new issue
Block a user