mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-11-06 12:52:04 +00:00
Make TUI v3 the default and add index0 foundations
This commit is contained in:
1 parent
0fa8aebc1b
commit
e8a2b81e7b
17 files changed
+3005
-76
No files matched your search
@@ -0,0 +1,130 @@
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
from helpers import create_vault
|
||||
from nostr.backup_models import ChunkMeta, Manifest
|
||||
from seedpass.core.index0 import (
|
||||
compute_event_hash,
|
||||
compute_event_id,
|
||||
ensure_index0_payload,
|
||||
merge_system_index0,
|
||||
normalize_index0,
|
||||
)
|
||||
|
||||
|
||||
def test_ensure_index0_payload_initializes_reserved_namespace():
|
||||
payload = ensure_index0_payload({"schema_version": 4, "entries": {}})
|
||||
|
||||
assert payload["_system"]["index0"]["schema_version"] == 1
|
||||
assert payload["_system"]["index0"]["events"] == {}
|
||||
assert payload["_system"]["index0"]["canonical_views"] == {}
|
||||
assert payload["_system"]["index0"]["stats"]["event_count"] == 0
|
||||
|
||||
|
||||
def test_merge_system_index0_is_order_independent_for_events_and_heads():
|
||||
base_event = {
|
||||
"event_type": "entry_modified",
|
||||
"subject_type": "entry",
|
||||
"subject_id": "1",
|
||||
"subject_kind": "password",
|
||||
"scope_path": "seed/root",
|
||||
"actor_type": "user",
|
||||
"actor_id": "fp1",
|
||||
"writer_id": "writer:profile:fp1",
|
||||
"modified_ts": 100,
|
||||
"payload_ref": {"entry_id": "1"},
|
||||
"links": [],
|
||||
"tags": ["b", "a"],
|
||||
"visibility": "private",
|
||||
"classification": "internal",
|
||||
"partition": "standard",
|
||||
}
|
||||
event_a = dict(base_event)
|
||||
event_a["integrity_hash"] = compute_event_hash(event_a)
|
||||
event_a["event_id"] = compute_event_id(event_a)
|
||||
event_b = dict(base_event)
|
||||
event_b["modified_ts"] = 200
|
||||
event_b["prev_hash"] = "prev"
|
||||
event_b["integrity_hash"] = compute_event_hash(event_b)
|
||||
event_b["event_id"] = compute_event_id(event_b)
|
||||
|
||||
current = normalize_index0(
|
||||
{
|
||||
"events": {event_a["event_id"]: event_a},
|
||||
"heads": {
|
||||
"writer:profile:fp1": {
|
||||
"event_id": event_a["event_id"],
|
||||
"head_hash": "aaa",
|
||||
"modified_ts": 100,
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
incoming = normalize_index0(
|
||||
{
|
||||
"events": {event_b["event_id"]: event_b},
|
||||
"heads": {
|
||||
"writer:profile:fp1": {
|
||||
"event_id": event_b["event_id"],
|
||||
"head_hash": "bbb",
|
||||
"modified_ts": 200,
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
ab = merge_system_index0(current, incoming)
|
||||
ba = merge_system_index0(incoming, current)
|
||||
|
||||
assert ab == ba
|
||||
assert ab["stats"]["event_count"] == 2
|
||||
assert ab["heads"]["writer:profile:fp1"]["head_hash"] == "bbb"
|
||||
|
||||
|
||||
def test_vault_save_and_load_index_normalizes_index0_namespace():
|
||||
with TemporaryDirectory() as tmpdir:
|
||||
base = Path(tmpdir)
|
||||
vault, _ = create_vault(base)
|
||||
vault.save_index({"schema_version": 4, "entries": {}})
|
||||
|
||||
loaded = vault.load_index()
|
||||
|
||||
assert "_system" in loaded
|
||||
assert "index0" in loaded["_system"]
|
||||
assert loaded["_system"]["index0"]["stats"]["writer_count"] == 0
|
||||
|
||||
|
||||
def test_manifest_supports_optional_index0_metadata_round_trip():
|
||||
manifest = Manifest(
|
||||
ver=1,
|
||||
algo="gzip",
|
||||
chunks=[ChunkMeta(id="c1", size=10, hash="abc", event_id="evt1")],
|
||||
delta_since=123,
|
||||
nonce="nonce",
|
||||
index0={
|
||||
"schema_version": 1,
|
||||
"checkpoint_ids": ["cp:day:2026-03-05:writer:profile:fp1"],
|
||||
"checkpoint_hashes": {"cp:day:2026-03-05:writer:profile:fp1": "abc123"},
|
||||
"stream_heads": {"writer:profile:fp1": "head123"},
|
||||
},
|
||||
)
|
||||
payload = {
|
||||
"ver": manifest.ver,
|
||||
"algo": manifest.algo,
|
||||
"chunks": [meta.__dict__ for meta in manifest.chunks],
|
||||
"delta_since": manifest.delta_since,
|
||||
"nonce": manifest.nonce,
|
||||
"index0": manifest.index0,
|
||||
}
|
||||
|
||||
parsed = Manifest(
|
||||
ver=payload["ver"],
|
||||
algo=payload["algo"],
|
||||
chunks=[ChunkMeta(**chunk) for chunk in payload["chunks"]],
|
||||
delta_since=payload["delta_since"],
|
||||
nonce=payload["nonce"],
|
||||
index0=payload["index0"],
|
||||
)
|
||||
|
||||
assert parsed.index0 is not None
|
||||
assert parsed.index0["stream_heads"]["writer:profile:fp1"] == "head123"
|
||||
@@ -5,9 +5,17 @@ from types import SimpleNamespace
|
||||
import pytest
|
||||
|
||||
pytest.importorskip("textual")
|
||||
from textual.widgets import DataTable
|
||||
from textual.widgets import DataTable, Input
|
||||
|
||||
from seedpass.tui_v3 import launch_tui3
|
||||
from seedpass.tui_v3.app import (
|
||||
CreateProfileScreen,
|
||||
RecoverProfileScreen,
|
||||
SeedPassTuiV3,
|
||||
SeedWordsScreen,
|
||||
SeedWordsReviewScreen,
|
||||
StartupScreen,
|
||||
)
|
||||
|
||||
|
||||
class V3EntryService:
|
||||
@@ -126,6 +134,7 @@ async def test_tui3_palette_lock_unlock_and_session_status() -> None:
|
||||
await pilot.pause()
|
||||
assert app.session_locked is True
|
||||
assert vault.locked is True
|
||||
assert isinstance(app.screen, StartupScreen)
|
||||
app.processor.execute("unlock hunter2")
|
||||
await pilot.pause()
|
||||
assert app.session_locked is False
|
||||
@@ -169,3 +178,245 @@ async def test_tui3_grid_focus_refresh_enables_selection_after_runtime_add() ->
|
||||
table._refresh_data()
|
||||
await pilot.pause()
|
||||
assert table.row_count >= 4
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_tui3_starts_with_unlock_screen_when_services_are_not_preloaded() -> None:
|
||||
app = SeedPassTuiV3()
|
||||
app._list_boot_profiles = lambda: [
|
||||
{"fingerprint": "EFBE51E70ED1B53A", "label": "Primary (EFBE51E70ED1B53A)"}
|
||||
]
|
||||
captured: dict[str, str] = {}
|
||||
|
||||
def _bootstrap(fingerprint: str, password: str) -> None:
|
||||
captured["fingerprint"] = fingerprint
|
||||
captured["password"] = password
|
||||
|
||||
app._bootstrap_profile_session = _bootstrap
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
assert isinstance(app.screen, StartupScreen)
|
||||
assert app.screen.query_one("#startup-profile-choice", Input).value == "1"
|
||||
app.screen.query_one("#startup-password", Input).value = "hunter2"
|
||||
await pilot.press("enter")
|
||||
await pilot.pause()
|
||||
assert captured == {
|
||||
"fingerprint": "EFBE51E70ED1B53A",
|
||||
"password": "hunter2",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_tui3_create_profile_screen_uses_in_app_bootstrap_helpers() -> None:
|
||||
app = SeedPassTuiV3()
|
||||
app._list_boot_profiles = lambda: []
|
||||
created: dict[str, str] = {}
|
||||
booted: dict[str, str] = {}
|
||||
|
||||
def _create_existing_profile(*, seed: str, password: str) -> str:
|
||||
created["seed"] = seed
|
||||
created["password"] = password
|
||||
return "NEWFP123"
|
||||
|
||||
def _bootstrap(fingerprint: str, password: str) -> None:
|
||||
booted["fingerprint"] = fingerprint
|
||||
booted["password"] = password
|
||||
|
||||
app._create_existing_profile = _create_existing_profile
|
||||
app._bootstrap_profile_session = _bootstrap
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
app.push_screen(CreateProfileScreen())
|
||||
await pilot.pause()
|
||||
screen = app.screen
|
||||
screen.query_one("#create-mode", Input).value = "existing"
|
||||
screen.query_one("#create-seed", Input).value = (
|
||||
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||
)
|
||||
screen.query_one("#create-password", Input).value = "hunter2"
|
||||
await pilot.press("enter")
|
||||
await pilot.pause()
|
||||
assert created["password"] == "hunter2"
|
||||
assert booted == {"fingerprint": "NEWFP123", "password": "hunter2"}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_tui3_recover_profile_screen_uses_in_app_bootstrap_helpers() -> None:
|
||||
app = SeedPassTuiV3()
|
||||
app._list_boot_profiles = lambda: [
|
||||
{"fingerprint": "EFBE51E70ED1B53A", "label": "Primary (EFBE51E70ED1B53A)"}
|
||||
]
|
||||
recovered: dict[str, str] = {}
|
||||
booted: dict[str, str] = {}
|
||||
|
||||
def _recover_profile(*, fingerprint: str, seed: str, password: str) -> None:
|
||||
recovered["fingerprint"] = fingerprint
|
||||
recovered["seed"] = seed
|
||||
recovered["password"] = password
|
||||
|
||||
def _bootstrap(fingerprint: str, password: str) -> None:
|
||||
booted["fingerprint"] = fingerprint
|
||||
booted["password"] = password
|
||||
|
||||
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 recovered["fingerprint"] == "EFBE51E70ED1B53A"
|
||||
assert booted == {
|
||||
"fingerprint": "EFBE51E70ED1B53A",
|
||||
"password": "hunter2",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_tui3_create_profile_screen_supports_nostr_restore_mode() -> None:
|
||||
app = SeedPassTuiV3()
|
||||
app._list_boot_profiles = lambda: []
|
||||
restored: dict[str, object] = {}
|
||||
booted: dict[str, str] = {}
|
||||
|
||||
def _restore_from_nostr_profile(
|
||||
*, seed: str, password: str, continue_without_backup: bool
|
||||
) -> str:
|
||||
restored["seed"] = seed
|
||||
restored["password"] = password
|
||||
restored["continue_without_backup"] = continue_without_backup
|
||||
return "NOSTRFP1"
|
||||
|
||||
def _bootstrap(fingerprint: str, password: str) -> None:
|
||||
booted["fingerprint"] = fingerprint
|
||||
booted["password"] = password
|
||||
|
||||
app._restore_from_nostr_profile = _restore_from_nostr_profile
|
||||
app._bootstrap_profile_session = _bootstrap
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
app.push_screen(CreateProfileScreen())
|
||||
await pilot.pause()
|
||||
screen = app.screen
|
||||
screen.query_one("#create-mode", Input).value = "nostr"
|
||||
screen.query_one("#create-seed", Input).value = (
|
||||
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||
)
|
||||
screen.query_one("#create-nostr-empty-ok", Input).value = "yes"
|
||||
screen.query_one("#create-password", Input).value = "hunter2"
|
||||
await pilot.press("enter")
|
||||
await pilot.pause()
|
||||
assert restored["continue_without_backup"] is True
|
||||
assert booted == {"fingerprint": "NOSTRFP1", "password": "hunter2"}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_tui3_create_profile_screen_supports_backup_restore_mode() -> None:
|
||||
app = SeedPassTuiV3()
|
||||
app._list_boot_profiles = lambda: []
|
||||
restored: dict[str, str] = {}
|
||||
booted: dict[str, str] = {}
|
||||
|
||||
def _restore_from_backup_profile(
|
||||
*, seed: str, password: str, backup_path: str
|
||||
) -> str:
|
||||
restored["seed"] = seed
|
||||
restored["password"] = password
|
||||
restored["backup_path"] = backup_path
|
||||
return "BACKUPFP1"
|
||||
|
||||
def _bootstrap(fingerprint: str, password: str) -> None:
|
||||
booted["fingerprint"] = fingerprint
|
||||
booted["password"] = password
|
||||
|
||||
app._restore_from_backup_profile = _restore_from_backup_profile
|
||||
app._bootstrap_profile_session = _bootstrap
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
app.push_screen(CreateProfileScreen())
|
||||
await pilot.pause()
|
||||
screen = app.screen
|
||||
screen.query_one("#create-mode", Input).value = "backup"
|
||||
screen.query_one("#create-seed", Input).value = (
|
||||
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||
)
|
||||
screen.query_one("#create-backup-path", Input).value = "/tmp/profile-backup.enc"
|
||||
screen.query_one("#create-password", Input).value = "hunter2"
|
||||
await pilot.press("enter")
|
||||
await pilot.pause()
|
||||
assert restored["backup_path"] == "/tmp/profile-backup.enc"
|
||||
assert booted == {"fingerprint": "BACKUPFP1", "password": "hunter2"}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_tui3_word_by_word_seed_entry_feeds_create_profile_screen() -> None:
|
||||
app = SeedPassTuiV3()
|
||||
app._list_boot_profiles = lambda: []
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
app.push_screen(CreateProfileScreen())
|
||||
await pilot.pause()
|
||||
create_screen = app.screen
|
||||
assert isinstance(create_screen, CreateProfileScreen)
|
||||
create_screen.query_one("#create-words").press()
|
||||
await pilot.pause()
|
||||
assert isinstance(app.screen, SeedWordsScreen)
|
||||
for idx, word in enumerate(
|
||||
[
|
||||
"abandon",
|
||||
"abandon",
|
||||
"abandon",
|
||||
"abandon",
|
||||
"abandon",
|
||||
"abandon",
|
||||
"abandon",
|
||||
"abandon",
|
||||
"abandon",
|
||||
"abandon",
|
||||
"abandon",
|
||||
"about",
|
||||
],
|
||||
start=1,
|
||||
):
|
||||
app.screen.query_one(f"#seed-word-{idx}", Input).value = word
|
||||
await pilot.press("enter")
|
||||
await pilot.pause()
|
||||
assert isinstance(app.screen, SeedWordsReviewScreen)
|
||||
app.screen.query_one("#seed-review-confirm").press()
|
||||
await pilot.pause()
|
||||
assert isinstance(app.screen, CreateProfileScreen)
|
||||
assert app.screen.query_one("#create-mode", Input).value == "words"
|
||||
assert (
|
||||
app.screen.query_one("#create-seed", Input).value
|
||||
== "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_tui3_word_entry_flags_invalid_bip39_word() -> None:
|
||||
app = SeedPassTuiV3()
|
||||
app._list_boot_profiles = lambda: []
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
app.push_screen(SeedWordsScreen(on_done=lambda _phrase: None))
|
||||
await pilot.pause()
|
||||
app.screen.query_one("#seed-word-1", Input).value = "notaword"
|
||||
await pilot.pause()
|
||||
status = str(app.screen.query_one("#seed-words-status").render())
|
||||
progress = str(app.screen.query_one("#seed-words-progress").render())
|
||||
assert "not in the BIP-39 wordlist" in status
|
||||
assert "invalid: 1" in progress
|
||||
+33
-17
@@ -684,6 +684,7 @@ def test_capabilities_json():
|
||||
payload = json.loads(result.stdout)
|
||||
assert payload["schema_version"] == 1
|
||||
assert "cli" in payload["interfaces"]
|
||||
assert payload["interfaces"]["cli"]["default_interface"] == "tui3"
|
||||
assert "agent" in payload["interfaces"]["cli"]["root_commands"]
|
||||
assert "tui3" in payload["interfaces"]["cli"]["root_commands"]
|
||||
assert "legacy" in payload["interfaces"]["cli"]["root_commands"]
|
||||
@@ -1011,8 +1012,23 @@ def test_update_checksum_command(monkeypatch):
|
||||
assert called.get("called") is True
|
||||
|
||||
|
||||
def test_root_tui2_forward_fingerprint(monkeypatch):
|
||||
"""Ensure --fingerprint is forwarded when launching default TUI v2."""
|
||||
def test_root_tui3_forward_fingerprint(monkeypatch):
|
||||
"""Ensure --fingerprint is forwarded when launching default TUI v3."""
|
||||
called = {}
|
||||
|
||||
def fake_launch(**kwargs):
|
||||
called["fp"] = kwargs.get("fingerprint")
|
||||
called["factory"] = kwargs.get("entry_service_factory")
|
||||
return True
|
||||
|
||||
monkeypatch.setattr(cli, "launch_tui3", fake_launch)
|
||||
result = runner.invoke(app, ["--fingerprint", "abc"])
|
||||
assert result.exit_code == 0
|
||||
assert called.get("fp") == "abc"
|
||||
assert called.get("factory") is None
|
||||
|
||||
|
||||
def test_root_explicit_tui2_forward_fingerprint(monkeypatch):
|
||||
called = {}
|
||||
|
||||
def fake_launch(**kwargs):
|
||||
@@ -1022,7 +1038,7 @@ def test_root_tui2_forward_fingerprint(monkeypatch):
|
||||
|
||||
monkeypatch.setattr(cli, "_get_entry_service", lambda _ctx: object())
|
||||
monkeypatch.setattr(cli, "launch_tui2", fake_launch)
|
||||
result = runner.invoke(app, ["--fingerprint", "abc"])
|
||||
result = runner.invoke(app, ["--tui", "v2", "--fingerprint", "abc"])
|
||||
assert result.exit_code == 0
|
||||
assert called.get("fp") == "abc"
|
||||
assert callable(called.get("factory"))
|
||||
@@ -1095,8 +1111,8 @@ def test_tui2_check(monkeypatch):
|
||||
def test_tui3_check(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
cli,
|
||||
"check_tui2_runtime",
|
||||
lambda: {"status": "ok", "backend": "textual", "textual_available": True},
|
||||
"check_tui3_runtime",
|
||||
lambda: {"status": "ok", "backend": "textual-v3", "textual_available": True},
|
||||
)
|
||||
result = runner.invoke(app, ["tui3", "--check"])
|
||||
assert result.exit_code == 0
|
||||
@@ -1153,24 +1169,24 @@ def test_tui2_fallback_legacy_strips_subcommand_argv(monkeypatch):
|
||||
assert called.get("argv") == []
|
||||
|
||||
|
||||
def test_root_fallback_legacy_when_tui2_unavailable(monkeypatch):
|
||||
called = {}
|
||||
monkeypatch.setattr(cli, "_get_entry_service", lambda _ctx: object())
|
||||
monkeypatch.setattr(cli, "launch_tui2", lambda **_: False)
|
||||
def test_root_tui3_unavailable_returns_error(monkeypatch):
|
||||
monkeypatch.setattr(cli, "launch_tui3", lambda **_: False)
|
||||
result = runner.invoke(app, ["--fingerprint", "abc"])
|
||||
assert result.exit_code == 1
|
||||
assert "TUI v3 runtime unavailable" in result.stderr
|
||||
assert "seedpass tui2" in result.stderr
|
||||
assert "seedpass legacy" in result.stderr
|
||||
|
||||
def fake_main(*, fingerprint=None):
|
||||
called["fp"] = fingerprint
|
||||
return 0
|
||||
|
||||
def test_root_tui3_does_not_preflight_entry_service(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
cli,
|
||||
"importlib",
|
||||
SimpleNamespace(import_module=lambda _: SimpleNamespace(main=fake_main)),
|
||||
"_get_entry_service",
|
||||
lambda _ctx: (_ for _ in ()).throw(RuntimeError("should not run")),
|
||||
)
|
||||
result = runner.invoke(app, ["--fingerprint", "abc"])
|
||||
monkeypatch.setattr(cli, "launch_tui3", lambda **_: True)
|
||||
result = runner.invoke(app, [])
|
||||
assert result.exit_code == 0
|
||||
assert called.get("fp") == "abc"
|
||||
assert "falling back to legacy TUI" in result.stdout
|
||||
|
||||
|
||||
def test_tui2_preflight_failure_without_fallback(monkeypatch):
|
||||
|
||||
Reference in new issue
Block a user