mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-11-04 20:02:05 +00:00
Add TUI v2 parity scenarios and align help hints
This commit is contained in:
1 parent
733a70c16f
commit
5f0dbe82af
6 files changed
+184
-3
No files matched your search
@@ -44,5 +44,7 @@ This directory contains SeedPass project documentation.
|
||||
|
||||
- `seedpass --help`
|
||||
- `seedpass <group> --help`
|
||||
- `seedpass tui2 --help`
|
||||
- `seedpass tui2 --check`
|
||||
- `seedpass capabilities --format json`
|
||||
- `seedpass agent bootstrap-context`
|
||||
@@ -43,7 +43,8 @@ This checklist tracks readiness to switch default interactive mode from legacy T
|
||||
|
||||
## Validation Checklist Before Cutover
|
||||
|
||||
- [ ] Define and run deterministic parity scenarios across entry kinds.
|
||||
- [x] Define and run deterministic parity scenarios across entry kinds.
|
||||
- `src/tests/test_tui_v2_parity_scenarios.py`
|
||||
- [x] Add Textual interaction tests for list/search/edit/link/pagination flows.
|
||||
- `src/tests/test_tui_v2_textual_interactions.py` (skips when `textual` is unavailable).
|
||||
- [x] Add non-UI unit tests for TUI v2 helper logic.
|
||||
@@ -51,4 +52,5 @@ This checklist tracks readiness to switch default interactive mode from legacy T
|
||||
- [x] Verify large vault behavior with realistic data volume in CI-like environment.
|
||||
- `src/tests/test_tui_v2_large_vault_validation.py`
|
||||
- standard: `10,000` rows, stress: `50,000` rows (`--stress`)
|
||||
- [ ] Confirm help text and docs match shipped keybindings and palette commands.
|
||||
- [x] Confirm help text and docs match shipped keybindings and palette commands.
|
||||
- Updated `seedpass tui2 --help`, capabilities hints, and docs discoverability commands.
|
||||
@@ -196,3 +196,5 @@
|
||||
- Introduced a test hook in `launch_tui2(..., app_hook=...)` so tests can capture and pilot the Textual app instance without starting a live terminal run.
|
||||
- 2026-03-01: Added CI-like large-vault validation with explicit thresholds in `src/tests/test_tui_v2_large_vault_validation.py` (10k rows standard, 50k rows under `--stress`).
|
||||
- Added docs `docs/tui_v2_large_vault_validation.md` with exact commands and budgets; parity checklist now marks large-vault CI-like validation complete.
|
||||
- 2026-03-01: Added deterministic TUI v2 parity scenarios across entry kinds in `src/tests/test_tui_v2_parity_scenarios.py` (kind filters, archive/restore roundtrip, non-document edit guard).
|
||||
- Completed help/docs alignment gate: updated `seedpass tui2` help text, capabilities hints for keybindings/palette (`Ctrl+P`, `p/n`, `x`), and docs discoverability commands (`seedpass tui2 --help/--check`).
|
||||
@@ -121,7 +121,7 @@ def tui2(
|
||||
help="Fall back to legacy TUI when TUI v2 runtime is unavailable",
|
||||
),
|
||||
) -> None:
|
||||
"""Launch experimental TUI v2 scaffold."""
|
||||
"""Launch the Textual-based TUI v2 (search/edit/graph workflows)."""
|
||||
if check:
|
||||
typer.echo(json.dumps(check_tui2_runtime(), indent=2, sort_keys=True))
|
||||
return
|
||||
|
||||
@@ -241,6 +241,7 @@ def build_capabilities() -> dict[str, Any]:
|
||||
"Run `seedpass <group> --help` for detailed options.",
|
||||
"Use `seedpass capabilities --format json` for machine-readable discovery.",
|
||||
"Run `seedpass tui2 --check` before adopting Textual-based TUI v2.",
|
||||
"In TUI v2, use `Ctrl+P` for the command palette, `p/n` for paging, and `x` to retry the last failed action.",
|
||||
"For graph workflows, start with `seedpass entry link-add --help` and `seedpass entry links --help`.",
|
||||
"For document workflows, run `seedpass entry import-document --help` and `seedpass agent document-import --help`.",
|
||||
"After unlock/login, run `seedpass capabilities` before autonomous runs.",
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
pytest.importorskip("textual")
|
||||
from textual.widgets import ListView, Static
|
||||
|
||||
from seedpass.tui_v2.app import launch_tui2
|
||||
|
||||
|
||||
class FakeEntryService:
|
||||
def __init__(self, entries: list[dict]) -> None:
|
||||
self._entries = {int(entry["id"]): dict(entry) for entry in entries}
|
||||
self._links = {
|
||||
int(entry["id"]): [dict(link) for link in entry.get("links", [])]
|
||||
for entry in entries
|
||||
}
|
||||
|
||||
def search_entries(self, query: str, kinds: list[str] | None = None):
|
||||
q = (query or "").strip().lower()
|
||||
out = []
|
||||
for entry_id in sorted(self._entries.keys()):
|
||||
entry = self._entries[entry_id]
|
||||
kind = str(entry.get("kind", "password"))
|
||||
if kinds and kind not in kinds:
|
||||
continue
|
||||
label = str(entry.get("label", ""))
|
||||
if q and q not in label.lower():
|
||||
continue
|
||||
out.append(
|
||||
(
|
||||
entry_id,
|
||||
label,
|
||||
None,
|
||||
None,
|
||||
bool(entry.get("archived", False)),
|
||||
SimpleNamespace(value=kind),
|
||||
)
|
||||
)
|
||||
return out
|
||||
|
||||
def retrieve_entry(self, entry_id: int):
|
||||
return dict(self._entries.get(int(entry_id), {}))
|
||||
|
||||
def modify_entry(self, entry_id: int, **kwargs) -> None:
|
||||
entry = self._entries[int(entry_id)]
|
||||
for key, value in kwargs.items():
|
||||
if value is None:
|
||||
continue
|
||||
entry[key] = value
|
||||
|
||||
def archive_entry(self, entry_id: int) -> None:
|
||||
self._entries[int(entry_id)]["archived"] = True
|
||||
|
||||
def restore_entry(self, entry_id: int) -> None:
|
||||
self._entries[int(entry_id)]["archived"] = False
|
||||
|
||||
def get_links(self, entry_id: int):
|
||||
return [dict(link) for link in self._links.get(int(entry_id), [])]
|
||||
|
||||
def add_link(
|
||||
self,
|
||||
entry_id: int,
|
||||
target_id: int,
|
||||
*,
|
||||
relation: str = "related_to",
|
||||
note: str = "",
|
||||
):
|
||||
links = self._links.setdefault(int(entry_id), [])
|
||||
links.append({"target": int(target_id), "relation": relation, "note": note})
|
||||
return [dict(link) for link in links]
|
||||
|
||||
def remove_link(
|
||||
self, entry_id: int, target_id: int, *, relation: str | None = None
|
||||
):
|
||||
src = self._links.setdefault(int(entry_id), [])
|
||||
keep = []
|
||||
for link in src:
|
||||
if int(link.get("target", -1)) != int(target_id):
|
||||
keep.append(link)
|
||||
continue
|
||||
if relation is not None and str(link.get("relation")) != relation:
|
||||
keep.append(link)
|
||||
self._links[int(entry_id)] = keep
|
||||
return [dict(link) for link in keep]
|
||||
|
||||
|
||||
def _build_app(service: FakeEntryService):
|
||||
holder: dict[str, object] = {}
|
||||
|
||||
def _hook(app):
|
||||
holder["app"] = app
|
||||
|
||||
launched = launch_tui2(entry_service_factory=lambda: service, app_hook=_hook)
|
||||
assert launched is True
|
||||
app = holder.get("app")
|
||||
assert app is not None
|
||||
return app
|
||||
|
||||
|
||||
async def _run_palette(pilot, command: str) -> None:
|
||||
await pilot.press("ctrl+p")
|
||||
await pilot.type(command)
|
||||
await pilot.press("enter")
|
||||
await pilot.pause()
|
||||
|
||||
|
||||
def _status_text(app) -> str:
|
||||
return str(app.query_one("#status", Static).renderable)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_tui2_parity_filters_cover_all_entry_kinds() -> None:
|
||||
kinds = [
|
||||
"password",
|
||||
"totp",
|
||||
"document",
|
||||
"key_value",
|
||||
"ssh",
|
||||
"pgp",
|
||||
"nostr",
|
||||
"seed",
|
||||
"managed_account",
|
||||
]
|
||||
entries = [
|
||||
{"id": i + 1, "kind": kind, "label": f"{kind}-entry", "content": "x"}
|
||||
for i, kind in enumerate(kinds)
|
||||
]
|
||||
app = _build_app(FakeEntryService(entries))
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
list_view = app.query_one("#entry-list", ListView)
|
||||
assert len(list_view.children) == len(kinds)
|
||||
|
||||
for kind in kinds:
|
||||
await _run_palette(pilot, f"filter {kind}")
|
||||
assert len(list_view.children) == 1
|
||||
detail_text = str(app.query_one("#entry-detail", Static).renderable)
|
||||
assert f'"kind": "{kind}"' in detail_text
|
||||
|
||||
await _run_palette(pilot, "filter all")
|
||||
assert len(list_view.children) == len(kinds)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_tui2_parity_archive_restore_roundtrip_non_document() -> None:
|
||||
service = FakeEntryService(
|
||||
[{"id": 1, "kind": "password", "label": "pw-entry", "archived": False}]
|
||||
)
|
||||
app = _build_app(service)
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
await _run_palette(pilot, "archive")
|
||||
assert service.retrieve_entry(1)["archived"] is True
|
||||
assert "archived" in _status_text(app)
|
||||
|
||||
await _run_palette(pilot, "restore")
|
||||
assert service.retrieve_entry(1)["archived"] is False
|
||||
assert "restored" in _status_text(app)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_tui2_parity_edit_document_guard_for_non_document() -> None:
|
||||
app = _build_app(FakeEntryService([{"id": 1, "kind": "password", "label": "pw"}]))
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
await pilot.press("e")
|
||||
await pilot.pause()
|
||||
assert "Select a document entry to edit" in _status_text(app)
|
||||
Reference in new issue
Block a user