mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-10-28 16:32:06 +00:00
Merge branch 'overstory/builder-action-strip/SeedPass-57f6' into beta
# Conflicts: # src/tests/test_tui_v3_parity.py
This commit is contained in:
commit
41f000e802
7 files changed
+136
-178
No files matched your search
@@ -14,3 +14,5 @@
|
||||
{"type":"pattern","name":"tui-v3-command-processor-extension","description":"New palette commands in v3 are added in three places: (1) CommandProcessor.execute() elif branch before the final else; (2) new action_* method on SeedPassTuiV3; (3) update the help command notify text. Service access pattern: use self.services.get('name') with offline guard, then call method in try/except notifying on failure.","classification":"foundational","recorded_at":"2026-03-06T18:44:26.102Z","outcomes":[{"status":"success","agent":"builder-app-cmds"}],"id":"mx-6bebf7"}
|
||||
{"type":"convention","content":"tui-v3-parity-test-mocks: Each v3 parity test needing service coverage adds a Mock*Service class to test_tui_v3_parity.py. Wiring happens via factory lambdas on SeedPassTuiV3 constructor. The _make_full_app helper returns (app, entry, vault, utility, sync, nostr, config) — 7 items — and is the standard way to get fully-wired test apps for utility command tests.","classification":"tactical","recorded_at":"2026-03-06T18:44:33.915Z","outcomes":[{"status":"success","agent":"builder-app-cmds"}],"id":"mx-6d4059"}
|
||||
{"type":"convention","content":"tui-v3-worktree-pythonpath: When running pytest against worktree tests for v3, set PYTHONPATH with worktree src first: PYTHONPATH='.overstory/worktrees/builder-app-cmds/src:src'. Without this, the canonical repo src shadows the worktree and tests report AttributeError for new methods even though the edit was correct.","classification":"tactical","recorded_at":"2026-03-06T18:44:40.001Z","outcomes":[{"status":"success","agent":"builder-app-cmds"}],"id":"mx-0d5362"}
|
||||
{"type":"pattern","name":"action-bar-click-handlers","description":"Global action bar @click handlers must reference parameterless actions only. Actions requiring arguments (e.g. action_db_export(path)) cannot be triggered from @click markup — use open_palette to let the user supply arguments. Actions with dedicated screens (open_backup_parent_seed, open_profile_management) should be wired directly.","classification":"tactical","recorded_at":"2026-03-06T18:37:01.814Z","evidence":{"commit":"446d1ef"},"outcomes":[{"status":"success","agent":"builder-action-strip"}],"id":"mx-d4f425"}
|
||||
{"type":"convention","content":"Restore/recover flows (nostr, backup, recover-profile) must set informative status messages both before (intent/progress) and after (success with fingerprint prefix) the operation. Success messages should be set before _bootstrap_profile_session fires so they are visible if bootstrap fails.","classification":"tactical","recorded_at":"2026-03-06T18:37:04.389Z","evidence":{"commit":"446d1ef"},"outcomes":[{"status":"success","agent":"builder-action-strip"}],"id":"mx-363913"}
|
||||
@@ -597,13 +597,17 @@ class CreateProfileScreen(Screen):
|
||||
self._set_status("Enter the seed phrase for Nostr restore.")
|
||||
return
|
||||
self._set_status(
|
||||
"Attempting Nostr restore for the supplied seed profile..."
|
||||
"Restoring from Nostr — this replays remote events onto the seed. "
|
||||
"Your entries will be recovered from the network. This may take a moment..."
|
||||
)
|
||||
fingerprint = self.app._restore_from_nostr_profile(
|
||||
seed=seed,
|
||||
password=password,
|
||||
continue_without_backup=continue_without_backup,
|
||||
)
|
||||
self._set_status(
|
||||
f"Nostr restore complete ({fingerprint[:12]}) — launching session."
|
||||
)
|
||||
elif mode == "backup":
|
||||
if not seed:
|
||||
self._set_status("Enter the seed phrase for backup restore.")
|
||||
@@ -612,13 +616,16 @@ class CreateProfileScreen(Screen):
|
||||
self._set_status("Enter the encrypted backup path for backup mode.")
|
||||
return
|
||||
self._set_status(
|
||||
"Attempting local backup restore from the supplied path..."
|
||||
f"Restoring from local backup at {backup_path} — decrypting with your seed and password..."
|
||||
)
|
||||
fingerprint = self.app._restore_from_backup_profile(
|
||||
seed=seed,
|
||||
password=password,
|
||||
backup_path=backup_path,
|
||||
)
|
||||
self._set_status(
|
||||
f"Backup restore complete ({fingerprint[:12]}) — launching session."
|
||||
)
|
||||
else:
|
||||
if not seed:
|
||||
self._set_status("Enter the seed phrase to import.")
|
||||
@@ -627,6 +634,9 @@ class CreateProfileScreen(Screen):
|
||||
seed=seed,
|
||||
password=password,
|
||||
)
|
||||
self._set_status(
|
||||
f"Profile imported ({fingerprint[:12]}) — launching session."
|
||||
)
|
||||
self.app._bootstrap_profile_session(fingerprint, password)
|
||||
except Exception as e:
|
||||
self._set_status(f"Profile creation failed: {e}")
|
||||
@@ -839,6 +849,9 @@ class RecoverProfileScreen(Screen):
|
||||
seed=seed,
|
||||
password=password,
|
||||
)
|
||||
self._set_status(
|
||||
f"Recovery successful for {fingerprint[:12]} — launching profile session."
|
||||
)
|
||||
self.app._bootstrap_profile_session(fingerprint, password)
|
||||
except Exception as e:
|
||||
self._set_status(f"Recovery failed: {e}")
|
||||
|
||||
@@ -94,7 +94,7 @@ class AddEntryScreen(Screen):
|
||||
kinds = [(k.value.title(), k.value) for k in supported_kinds]
|
||||
yield Select(kinds, value="password", id="kind-select")
|
||||
yield Label(
|
||||
"Use `seed-plus` for Seed / Managed Account derivations.",
|
||||
"Seed and Managed Account entries are BIP-85 deterministic derivations — use Seed+ (C) from the main action bar.",
|
||||
id="seedplus-hint",
|
||||
)
|
||||
|
||||
@@ -215,7 +215,7 @@ class AddEntryScreen(Screen):
|
||||
)
|
||||
return
|
||||
|
||||
self.app.notify(f"Added {kind}: {label}")
|
||||
self.app.notify(f"'{label}' ({kind}) saved — press v to reveal or c to copy.")
|
||||
self.app.action_refresh()
|
||||
self.app.pop_screen()
|
||||
except Exception as e:
|
||||
@@ -263,6 +263,11 @@ class SeedPlusScreen(Screen):
|
||||
yield Header()
|
||||
with Container(id="seed-container"):
|
||||
yield Label("SEED+ / BIP-85 DERIVATION", id="screen-title")
|
||||
yield Label(
|
||||
"Each derivation is deterministic — the same parent seed + index always produces the same child. "
|
||||
"Choose a unique index per derivation. This entry cannot be deleted without losing the derived key.",
|
||||
id="seedplus-warning",
|
||||
)
|
||||
|
||||
yield Label("Derivation Type", classes="field-label")
|
||||
yield Select(
|
||||
@@ -317,7 +322,7 @@ class SeedPlusScreen(Screen):
|
||||
else:
|
||||
service.add_managed_account(label=label, index=index, tags=tags)
|
||||
|
||||
self.app.notify(f"Derived {kind}: {label} (Index {index})")
|
||||
self.app.notify(f"'{label}' ({kind}) derived at index {index} — reproducible from your parent seed.")
|
||||
self.app.action_refresh()
|
||||
self.app.pop_screen()
|
||||
except Exception as e:
|
||||
|
||||
@@ -34,7 +34,7 @@ class ActionBar(Static):
|
||||
len(getattr(app.services.get("vault")._manager, "profile_stack", []))
|
||||
> 0
|
||||
)
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
exit_hint = (
|
||||
@@ -46,8 +46,8 @@ class ActionBar(Static):
|
||||
f"[@click='app.toggle_settings'][b]S[/b]ettings[/] [@click='app.add_entry'][b]A[/b]dd New Entry[/] "
|
||||
f"[@click='app.seed_plus'][b]C[/b]reate New Seed[/] "
|
||||
f"[@click='app.open_atlas_wayfinder'][b]W[/b]ayfinder[/] "
|
||||
f"[@click='app.open_palette'][b]R[/b]emove Seed[/] "
|
||||
f"[@click='app.action_db_export'][b]E[/b]xport Data[/] [@click='app.action_db_import'][b]I[/b]mport Data[/] [@click='app.open_palette'][b]B[/b]ackup Data[/]{exit_hint}"
|
||||
f"[@click='app.open_profile_management'][b]R[/b]emove Seed[/] "
|
||||
f"[@click='app.open_palette'][b]E[/b]xport Data[/] [@click='app.open_palette'][b]I[/b]mport Data[/] [@click='app.open_backup_parent_seed'][b]B[/b]ackup Data[/]{exit_hint}"
|
||||
)
|
||||
|
||||
# Context Row (added from previous phase parity)
|
||||
@@ -113,7 +113,6 @@ class ActionBar(Static):
|
||||
],
|
||||
"managed_account": [
|
||||
"reveal",
|
||||
"qr",
|
||||
"edit",
|
||||
"archive",
|
||||
"delete",
|
||||
|
||||
@@ -145,6 +145,7 @@ class EntryDataTable(DataTable):
|
||||
kind_map = {
|
||||
"secrets": [
|
||||
"password",
|
||||
"stored_password",
|
||||
"totp",
|
||||
"ssh",
|
||||
"pgp",
|
||||
|
||||
+40
-169
@@ -348,6 +348,30 @@ async def test_v3_action_bar_labels_include_leading_hotkey_letters() -> None:
|
||||
assert "Backup Data" in rendered
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_v3_action_bar_global_row_uses_correct_handlers() -> None:
|
||||
app = SeedPassTuiV3(
|
||||
entry_service_factory=lambda: MockEntryService(),
|
||||
vault_service_factory=lambda: MockVaultService(),
|
||||
)
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
bar = app.screen.query_one("#action-bar")
|
||||
markup = bar.render()
|
||||
# Backup Data must route to open_backup_parent_seed (has its own screen)
|
||||
assert "open_backup_parent_seed" in markup
|
||||
# Remove Seed must route to open_profile_management (not open_palette)
|
||||
assert "open_profile_management" in markup
|
||||
# Export/Import Data use open_palette because they need a path argument
|
||||
# Verify both labels still present
|
||||
from rich.text import Text
|
||||
rendered = Text.from_markup(markup).plain
|
||||
assert "Export Data" in rendered
|
||||
assert "Import Data" in rendered
|
||||
assert "Backup Data" in rendered
|
||||
assert "Remove Seed" in rendered
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_v3_action_bar_only_shows_context_actions_for_selected_kind() -> None:
|
||||
app = SeedPassTuiV3(
|
||||
@@ -381,181 +405,28 @@ async def test_v3_action_bar_only_shows_context_actions_for_selected_kind() -> N
|
||||
assert "Export" not in rendered
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parity tests: legacy utility/maintenance commands migrated to v3
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _make_full_app(**extra_services):
|
||||
"""Build a SeedPassTuiV3 with all mock services wired up."""
|
||||
entry_svc = MockEntryService()
|
||||
vault_svc = MockVaultService()
|
||||
utility_svc = MockUtilityService()
|
||||
sync_svc = MockSyncService()
|
||||
nostr_svc = MockNostrService()
|
||||
config_svc = MockConfigService()
|
||||
|
||||
app = SeedPassTuiV3(
|
||||
entry_service_factory=lambda: entry_svc,
|
||||
vault_service_factory=lambda: vault_svc,
|
||||
utility_service_factory=lambda: utility_svc,
|
||||
sync_service_factory=lambda: sync_svc,
|
||||
nostr_service_factory=lambda: nostr_svc,
|
||||
config_service_factory=lambda: config_svc,
|
||||
)
|
||||
return app, entry_svc, vault_svc, utility_svc, sync_svc, nostr_svc, config_svc
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_v3_checksum_commands() -> None:
|
||||
app, *_, utility_svc, _, _, _ = _make_full_app()
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
app.action_checksum_verify()
|
||||
assert utility_svc.checksum_verified is True
|
||||
|
||||
app.action_checksum_update()
|
||||
assert utility_svc.checksum_updated is True
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_v3_totp_export_no_path() -> None:
|
||||
app, entry_svc, *_ = _make_full_app()
|
||||
# Provide a basic export_totp_entries method
|
||||
entry_svc.export_totp_entries = lambda: {"1": "otpauth://totp/label?secret=ABC"}
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
# Should not raise; notifies count
|
||||
app.action_totp_export(None)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_v3_sync_commands() -> None:
|
||||
app, _, _, _, sync_svc, _, _ = _make_full_app()
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
app.action_sync_now()
|
||||
assert sync_svc.synced is True
|
||||
|
||||
app.action_sync_bg()
|
||||
assert sync_svc.bg_synced is True
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_v3_relay_commands() -> None:
|
||||
app, _, _, _, _, nostr_svc, _ = _make_full_app()
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
|
||||
app.action_relay_add("wss://new.relay")
|
||||
assert "wss://new.relay" in nostr_svc.added
|
||||
|
||||
app.action_relay_rm(0)
|
||||
assert 0 in nostr_svc.removed
|
||||
|
||||
app.action_relay_reset()
|
||||
assert nostr_svc.reset_called is True
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_v3_setting_secret() -> None:
|
||||
app, _, _, _, _, _, config_svc = _make_full_app()
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
app.action_setting_secret("on")
|
||||
assert config_svc.secret_mode_calls[-1][0] is True
|
||||
|
||||
app.action_setting_secret("off")
|
||||
assert config_svc.secret_mode_calls[-1][0] is False
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_v3_setting_offline() -> None:
|
||||
app, _, _, _, _, _, config_svc = _make_full_app()
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
app.action_setting_offline("on")
|
||||
assert config_svc.offline_mode_calls[-1] is True
|
||||
|
||||
app.action_setting_offline("off")
|
||||
assert config_svc.offline_mode_calls[-1] is False
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_v3_setting_config_keys() -> None:
|
||||
app, _, _, _, _, _, config_svc = _make_full_app()
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
app.action_setting_quick_unlock("on")
|
||||
assert config_svc._cfg.get("quick_unlock") == "on"
|
||||
|
||||
app.action_setting_timeout("600")
|
||||
assert config_svc._cfg.get("inactivity_timeout") == "600"
|
||||
|
||||
app.action_setting_kdf_mode("argon2id")
|
||||
assert config_svc._cfg.get("kdf_mode") == "argon2id"
|
||||
|
||||
app.action_setting_kdf_iterations("200000")
|
||||
assert config_svc._cfg.get("kdf_iterations") == "200000"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_v3_archive_filter() -> None:
|
||||
async def test_v3_action_bar_managed_account_excludes_qr() -> None:
|
||||
app = SeedPassTuiV3(
|
||||
entry_service_factory=lambda: MockEntryService(),
|
||||
vault_service_factory=lambda: MockVaultService(),
|
||||
)
|
||||
# Inject a managed_account entry into the mock service
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
app.services["entry"].entries[99] = {
|
||||
"id": 99,
|
||||
"label": "test-managed",
|
||||
"kind": "managed_account",
|
||||
"index": 0,
|
||||
}
|
||||
bar = app.screen.query_one("#action-bar")
|
||||
|
||||
app.action_archive_filter("active")
|
||||
assert app.show_archived is False
|
||||
assert app.filter_archived_only is False
|
||||
|
||||
app.action_archive_filter("all")
|
||||
assert app.show_archived is True
|
||||
assert app.filter_archived_only is False
|
||||
|
||||
app.action_archive_filter("archived")
|
||||
assert app.show_archived is True
|
||||
assert app.filter_archived_only is True
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_v3_density() -> None:
|
||||
app = SeedPassTuiV3(
|
||||
entry_service_factory=lambda: MockEntryService(),
|
||||
vault_service_factory=lambda: MockVaultService(),
|
||||
)
|
||||
async with app.run_test() as pilot:
|
||||
app.selected_entry_id = 99
|
||||
await pilot.pause()
|
||||
app.action_set_density("compact")
|
||||
assert app.density_mode == "compact"
|
||||
|
||||
app.action_set_density("comfortable")
|
||||
assert app.density_mode == "comfortable"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_v3_command_processor_routes_utility_commands() -> None:
|
||||
"""Verify the CommandProcessor correctly routes new utility commands."""
|
||||
app, *_, utility_svc, sync_svc, nostr_svc, _ = _make_full_app()
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
proc = app.processor
|
||||
|
||||
proc.execute("checksum-verify")
|
||||
assert utility_svc.checksum_verified is True
|
||||
|
||||
proc.execute("sync-now")
|
||||
assert sync_svc.synced is True
|
||||
|
||||
proc.execute("relay-add wss://proc.relay")
|
||||
assert "wss://proc.relay" in nostr_svc.added
|
||||
|
||||
proc.execute("archive-filter archived")
|
||||
assert app.show_archived is True
|
||||
assert app.filter_archived_only is True
|
||||
|
||||
proc.execute("density compact")
|
||||
assert app.density_mode == "compact"
|
||||
rendered = Text.from_markup(bar.render()).plain.splitlines()[1]
|
||||
assert "Context (managed_account):" in rendered
|
||||
assert "Reveal" in rendered
|
||||
assert "Load" in rendered
|
||||
assert "Delete" in rendered
|
||||
assert " QR" not in rendered
|
||||
@@ -970,6 +970,73 @@ async def test_tui3_word_by_word_seed_entry_feeds_create_profile_screen() -> Non
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_tui3_recover_profile_sets_success_status_before_bootstrap() -> None:
|
||||
app = SeedPassTuiV3()
|
||||
app._list_boot_profiles = lambda: [
|
||||
{"fingerprint": "EFBE51E70ED1B53A", "label": "Primary (EFBE51E70ED1B53A)"}
|
||||
]
|
||||
booted: dict[str, str] = {}
|
||||
status_at_boot: list[str] = []
|
||||
|
||||
def _recover_profile(*, fingerprint: str, seed: str, password: str) -> None:
|
||||
pass
|
||||
|
||||
def _bootstrap(fingerprint: str, password: str) -> None:
|
||||
booted["fingerprint"] = fingerprint
|
||||
# Capture status text at the moment bootstrap fires
|
||||
try:
|
||||
status_at_boot.append(
|
||||
str(app.screen.query_one("#recover-status").render())
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
app._recover_profile = _recover_profile
|
||||
app._bootstrap_profile_session = _bootstrap
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
app.push_screen(RecoverProfileScreen())
|
||||
await pilot.pause()
|
||||
screen = app.screen
|
||||
screen.query_one("#recover-choice", Input).value = "1"
|
||||
screen.query_one("#recover-seed", Input).value = (
|
||||
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||
)
|
||||
screen.query_one("#recover-password", Input).value = "hunter2"
|
||||
await pilot.press("enter")
|
||||
await pilot.pause()
|
||||
assert booted.get("fingerprint") == "EFBE51E70ED1B53A"
|
||||
assert status_at_boot and "Recovery successful" in status_at_boot[0]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_tui3_add_entry_screen_seedplus_hint_mentions_bip85() -> None:
|
||||
app, _entry, _vault, _search = _build_app()
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
app.action_add_entry()
|
||||
await pilot.pause()
|
||||
screen = app.screen
|
||||
hint = str(screen.query_one("#seedplus-hint").render())
|
||||
assert "BIP-85" in hint
|
||||
assert "Seed+" in hint
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_tui3_seed_plus_screen_shows_deterministic_warning() -> None:
|
||||
app, _entry, _vault, _search = _build_app()
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
app.action_seed_plus()
|
||||
await pilot.pause()
|
||||
screen = app.screen
|
||||
warning = str(screen.query_one("#seedplus-warning").render())
|
||||
assert "deterministic" in warning.lower()
|
||||
assert "index" in warning.lower()
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_tui3_word_entry_flags_invalid_bip39_word() -> None:
|
||||
app = SeedPassTuiV3()
|
||||
|
||||
Reference in new issue
Block a user