feat(tui-v3): implement core functional parity (Phase A/B)

- Implement secure data parity (Reveal/QR/Copy) for SSH, PGP, Nostr, Key-Value, and Documents.
- Update derive_pgp_key and API to support PGP Public Key retrieval and copying.
- Implement managed account loading/exiting (ml/mx) with reactive profile updates.
- Integrate semantic search (keyword/hybrid/semantic) into the entry grid.
- Create AddEntryScreen (unified wizard), SeedPlusScreen (BIP-85), and DocumentEditScreen.
- Add grid filter presets (secrets, docs, keys, 2fa) and archive view toggle.
- Update ActionBar and GridMetrics to be dynamic and context-aware.
- Add comprehensive v3 parity tests and update existing core tests for PGP tuple changes.
This commit is contained in:
thePR0M3TH3AN committed 2026-03-03 07:40:34 -05:00
1 parent d3920ccc4c
commit c822eeb602
17 files changed
+855 -89

No files matched your search

+2 -2
View File
@@ -337,12 +337,12 @@ class TestEntryService:
assert result == ("priv", "pub")
def test_get_pgp_key(self, service, mock_manager):
mock_manager.entry_manager.get_pgp_key.return_value = ("priv", "fp")
mock_manager.entry_manager.get_pgp_key.return_value = ("priv", "pub", "fp")
result = service.get_pgp_key(4)
mock_manager.entry_manager.get_pgp_key.assert_called_once_with(
4, mock_manager.parent_seed
)
assert result == ("priv", "fp")
assert result == ("priv", "pub", "fp")
def test_get_nostr_key_pair(self, service, mock_manager):
mock_manager.entry_manager.get_nostr_key_pair.return_value = ("npub", "nsec")
+2 -2
View File
@@ -227,7 +227,7 @@ def test_show_pgp_entry_details(monkeypatch, capsys):
tmp_path = Path(tmpdir)
pm, entry_mgr = _setup_manager(tmp_path)
idx = entry_mgr.add_pgp_key("pgp", TEST_SEED, user_id="test")
_k, fp = entry_mgr.get_pgp_key(idx, TEST_SEED)
_k, _pub, fp = entry_mgr.get_pgp_key(idx, TEST_SEED)
called = _detail_common(monkeypatch, pm)
@@ -335,7 +335,7 @@ def test_show_entry_details_sensitive(monkeypatch, capsys, entry_type):
extra = pub
elif entry_type == "pgp":
idx = entry_mgr.add_pgp_key("pgp", TEST_SEED, user_id="test")
priv, fp = entry_mgr.get_pgp_key(idx, TEST_SEED)
priv, pub, fp = entry_mgr.get_pgp_key(idx, TEST_SEED)
expected = priv
extra = fp
elif entry_type == "nostr":
+4 -4
View File
@@ -25,8 +25,8 @@ def test_pgp_key_determinism():
idx = entry_mgr.add_pgp_key(
"pgp", TEST_SEED, key_type="ed25519", user_id="Test"
)
key1, fp1 = entry_mgr.get_pgp_key(idx, TEST_SEED)
key2, fp2 = entry_mgr.get_pgp_key(idx, TEST_SEED)
key1, pub1, fp1 = entry_mgr.get_pgp_key(idx, TEST_SEED)
key2, pub2, fp2 = entry_mgr.get_pgp_key(idx, TEST_SEED)
assert fp1 == fp2
assert key1 == key2
@@ -55,8 +55,8 @@ def test_pgp_rsa_key_determinism():
entry_mgr = EntryManager(vault, backup_mgr)
idx = entry_mgr.add_pgp_key("pgp", TEST_SEED, key_type="rsa", user_id="Test")
key1, fp1 = entry_mgr.get_pgp_key(idx, TEST_SEED)
key2, fp2 = entry_mgr.get_pgp_key(idx, TEST_SEED)
key1, pub1, fp1 = entry_mgr.get_pgp_key(idx, TEST_SEED)
key2, pub2, fp2 = entry_mgr.get_pgp_key(idx, TEST_SEED)
assert fp1 == fp2
assert key1 == key2
+135
View File
@@ -0,0 +1,135 @@
from __future__ import annotations
import pytest
from unittest.mock import MagicMock
from textual.widgets import DataTable
from seedpass.tui_v3.app import SeedPassTuiV3
class MockEntryService:
def __init__(self):
self.entries = {
1: {"id": 1, "label": "test-pwd", "kind": "password", "username": "user1", "length": 16},
2: {"id": 2, "label": "test-ssh", "kind": "ssh", "index": 0},
3: {"id": 3, "label": "test-pgp", "kind": "pgp", "index": 0},
4: {"id": 4, "label": "test-nostr", "kind": "nostr", "index": 0},
5: {"id": 5, "label": "test-doc", "kind": "document", "content": "hello world"},
6: {"id": 6, "label": "test-seed", "kind": "seed", "index": 0},
}
def search_entries(self, query="", kinds=None, include_archived=False, archived_only=False):
results = []
for e in self.entries.values():
kind = e["kind"]
if kinds and kind not in kinds: continue
is_arch = e.get("archived", False)
if archived_only and not is_arch: continue
if not include_archived and is_arch: continue
results.append((e["id"], e["label"], e.get("username"), e.get("url"), is_arch, MagicMock(value=kind)))
return results
def retrieve_entry(self, eid):
return self.entries.get(eid)
def generate_password(self, length, eid):
return "gen-pwd"
def get_ssh_key_pair(self, eid):
return "priv", "pub"
def get_pgp_key(self, eid):
return "priv", "pub", "fp"
def get_nostr_key_pair(self, eid):
return "npub", "nsec"
def get_seed_phrase(self, eid, parent):
return "word1 word2"
def copy_to_clipboard(self, val):
return True
def archive_entry(self, eid):
self.entries[eid]["archived"] = True
def restore_entry(self, eid):
self.entries[eid]["archived"] = False
def load_managed_account(self, eid):
pass
def exit_managed_account(self):
pass
def add_entry(self, label, username=None, url=None, length=16, tags=None):
eid = max(self.entries.keys()) + 1
self.entries[eid] = {"id": eid, "label": label, "kind": "password", "username": username, "tags": tags}
return eid
def add_totp(self, label, secret, username=None, tags=None):
eid = max(self.entries.keys()) + 1
self.entries[eid] = {"id": eid, "label": label, "kind": "totp", "username": username, "tags": tags}
return eid
def add_seed(self, label, index=0, tags=None):
eid = max(self.entries.keys()) + 1
self.entries[eid] = {"id": eid, "label": label, "kind": "seed", "index": index, "tags": tags}
return eid
def add_managed_account(self, label, index=0, tags=None):
eid = max(self.entries.keys()) + 1
self.entries[eid] = {"id": eid, "label": label, "kind": "managed_account", "index": index, "tags": tags}
return eid
class MockVaultService:
def __init__(self):
self._manager = MagicMock()
self._manager.current_fingerprint = "parent-fp"
self._manager.profile_stack = []
def stats(self):
return {"total_entries": len(self._manager.entries) if hasattr(self._manager, "entries") else 6}
@pytest.mark.anyio
async def test_v3_reveal_copy_parity():
app = SeedPassTuiV3(
entry_service_factory=lambda: MockEntryService(),
vault_service_factory=lambda: MockVaultService(),
)
async with app.run_test() as pilot:
app.selected_entry_id = 1
app.action_reveal_selected(confirm=True)
payload = app._resolve_sensitive_payload()
assert payload[3] == "gen-pwd"
app.selected_entry_id = 4
payload = app._resolve_sensitive_payload()
assert payload[3] == "nsec"
@pytest.mark.anyio
async def test_v3_archive_restore():
app = SeedPassTuiV3(
entry_service_factory=lambda: MockEntryService(),
vault_service_factory=lambda: MockVaultService(),
)
async with app.run_test() as pilot:
app.selected_entry_id = 1
app.action_toggle_archive()
assert app.services["entry"].entries[1].get("archived") is True
app.action_toggle_archive()
assert app.services["entry"].entries[1].get("archived") is False
@pytest.mark.anyio
async def test_v3_add_entry():
app = SeedPassTuiV3(
entry_service_factory=lambda: MockEntryService(),
vault_service_factory=lambda: MockVaultService(),
)
async with app.run_test() as pilot:
app.action_add_entry()
await pilot.pause()
screen = app.screen
from textual.widgets import Input
screen.query_one("#entry-label", Input).value = "New Password"
screen.query_one("#entry-username", Input).value = "[email protected]"
await pilot.press("ctrl+s")
await pilot.pause()
assert any(e["label"] == "New Password" for e in app.services["entry"].entries.values())
@pytest.mark.anyio
async def test_v3_filters():
app = SeedPassTuiV3(
entry_service_factory=lambda: MockEntryService(),
vault_service_factory=lambda: MockVaultService(),
)
async with app.run_test() as pilot:
await pilot.pause()
table = app.screen.query_one("#entry-data-table", DataTable)
initial_count = table.row_count
app.action_set_kind_filter("2fa")
await pilot.pause()
assert table.row_count == 0 # No TOTP in mock
app.action_set_kind_filter("keys")
await pilot.pause()
assert table.row_count > 0