mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-10-30 01:12:17 +00:00
chore: format codebase and harden v3 CLI/CI gates
This commit is contained in:
1 parent
95e5a14b57
commit
538ce42338
27 files changed
+1309
-459
No files matched your search
@@ -3,8 +3,8 @@
|
||||
SeedPass TUI Interactive Agent Test Suite (v2 and v3 Support)
|
||||
=============================================================
|
||||
|
||||
This script provides a "Human-in-the-loop" style automated test for the
|
||||
SeedPass TUI. It bypasses real encryption and filesystem requirements
|
||||
This script provides a "Human-in-the-loop" style automated test for the
|
||||
SeedPass TUI. It bypasses real encryption and filesystem requirements
|
||||
by injecting a MockService layer.
|
||||
|
||||
Usage:
|
||||
@@ -25,45 +25,55 @@ sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
||||
from seedpass.tui_v2.app import launch_tui2
|
||||
from seedpass.tui_v3 import launch_tui3
|
||||
|
||||
|
||||
class AgentMockService:
|
||||
"""
|
||||
A comprehensive mock of all SeedPass core services.
|
||||
Bypasses real encryption and state.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.entries = {}
|
||||
self.next_id = 1
|
||||
self.archived = set()
|
||||
self.links = [] # list of dicts: {source, target, relation, note}
|
||||
self.links = [] # list of dicts: {source, target, relation, note}
|
||||
self.is_managed = False
|
||||
self.nostr_index = 0
|
||||
self.offline = False
|
||||
self._manager = SimpleNamespace(
|
||||
parent_seed="MOCK_PARENT_SEED",
|
||||
current_fingerprint="EFBE51E70ED1",
|
||||
profile_stack=[]
|
||||
parent_seed="MOCK_PARENT_SEED",
|
||||
current_fingerprint="EFBE51E70ED1",
|
||||
profile_stack=[],
|
||||
)
|
||||
|
||||
# --- EntryService Interface ---
|
||||
def search_entries(self, query="", kinds=None, include_archived=False, archived_only=False):
|
||||
def search_entries(
|
||||
self, query="", kinds=None, include_archived=False, archived_only=False
|
||||
):
|
||||
out = []
|
||||
q = (query or "").lower()
|
||||
for eid, entry in self.entries.items():
|
||||
is_archived = eid in self.archived
|
||||
if archived_only and not is_archived: continue
|
||||
if not include_archived and is_archived: continue
|
||||
if q and q not in entry["label"].lower(): continue
|
||||
if archived_only and not is_archived:
|
||||
continue
|
||||
if not include_archived and is_archived:
|
||||
continue
|
||||
if q and q not in entry["label"].lower():
|
||||
continue
|
||||
kind = entry["kind"]
|
||||
if kinds and kind not in kinds: continue
|
||||
|
||||
out.append((
|
||||
eid,
|
||||
entry["label"],
|
||||
entry.get("username"),
|
||||
entry.get("url"),
|
||||
is_archived,
|
||||
SimpleNamespace(value=kind)
|
||||
))
|
||||
if kinds and kind not in kinds:
|
||||
continue
|
||||
|
||||
out.append(
|
||||
(
|
||||
eid,
|
||||
entry["label"],
|
||||
entry.get("username"),
|
||||
entry.get("url"),
|
||||
is_archived,
|
||||
SimpleNamespace(value=kind),
|
||||
)
|
||||
)
|
||||
return sorted(out, key=lambda x: x[0])
|
||||
|
||||
def retrieve_entry(self, entry_id):
|
||||
@@ -75,7 +85,15 @@ class AgentMockService:
|
||||
|
||||
def add_entry(self, label, length, username=None, url=None, **kwargs):
|
||||
eid = self.next_id
|
||||
self.entries[eid] = {"id": eid, "label": label, "kind": "password", "username": username, "url": url, "length": length, **kwargs}
|
||||
self.entries[eid] = {
|
||||
"id": eid,
|
||||
"label": label,
|
||||
"kind": "password",
|
||||
"username": username,
|
||||
"url": url,
|
||||
"length": length,
|
||||
**kwargs,
|
||||
}
|
||||
self.next_id += 1
|
||||
return eid
|
||||
|
||||
@@ -87,7 +105,14 @@ class AgentMockService:
|
||||
|
||||
def add_document(self, label, content, file_type="txt", **kwargs):
|
||||
eid = self.next_id
|
||||
self.entries[eid] = {"id": eid, "label": label, "kind": "document", "content": content, "file_type": file_type, **kwargs}
|
||||
self.entries[eid] = {
|
||||
"id": eid,
|
||||
"label": label,
|
||||
"kind": "document",
|
||||
"content": content,
|
||||
"file_type": file_type,
|
||||
**kwargs,
|
||||
}
|
||||
self.next_id += 1
|
||||
return eid
|
||||
|
||||
@@ -96,11 +121,21 @@ class AgentMockService:
|
||||
if eid in self.entries:
|
||||
self.entries[eid].update(kwargs)
|
||||
|
||||
def archive_entry(self, entry_id): self.archived.add(int(entry_id))
|
||||
def restore_entry(self, entry_id): self.archived.discard(int(entry_id))
|
||||
def archive_entry(self, entry_id):
|
||||
self.archived.add(int(entry_id))
|
||||
|
||||
def restore_entry(self, entry_id):
|
||||
self.archived.discard(int(entry_id))
|
||||
|
||||
def add_link(self, source_id, target_id, relation="related_to", note=""):
|
||||
self.links.append({"source": int(source_id), "target": int(target_id), "relation": str(relation), "note": str(note)})
|
||||
self.links.append(
|
||||
{
|
||||
"source": int(source_id),
|
||||
"target": int(target_id),
|
||||
"relation": str(relation),
|
||||
"note": str(note),
|
||||
}
|
||||
)
|
||||
|
||||
def get_links(self, entry_id):
|
||||
eid = int(entry_id)
|
||||
@@ -110,49 +145,71 @@ class AgentMockService:
|
||||
def get(self, key):
|
||||
return {"inactivity_timeout": 300, "secret_mode_enabled": True}.get(key)
|
||||
|
||||
def unlock(self, req): return SimpleNamespace(status="ok", duration=0.1)
|
||||
def lock(self): pass
|
||||
def unlock(self, req):
|
||||
return SimpleNamespace(status="ok", duration=0.1)
|
||||
|
||||
def lock(self):
|
||||
pass
|
||||
|
||||
def stats(self):
|
||||
return {"total_entries": len(self.entries), "archived": len(self.archived)}
|
||||
|
||||
|
||||
def list_profiles(self):
|
||||
return ["EFBE51E70ED1B53A"]
|
||||
|
||||
def sync(self): return SimpleNamespace(manifest_id="agent-mock-manifest")
|
||||
def sync(self):
|
||||
return SimpleNamespace(manifest_id="agent-mock-manifest")
|
||||
|
||||
def start_fresh_namespace(self):
|
||||
self.nostr_index += 1
|
||||
return self.nostr_index
|
||||
|
||||
def load_managed_account(self, entry_id):
|
||||
|
||||
def load_managed_account(self, entry_id):
|
||||
self.is_managed = True
|
||||
self._manager.profile_stack.append((self._manager.current_fingerprint, "path", "seed"))
|
||||
self._manager.profile_stack.append(
|
||||
(self._manager.current_fingerprint, "path", "seed")
|
||||
)
|
||||
self._manager.current_fingerprint = f"SUBACCT{entry_id}"
|
||||
|
||||
def exit_managed_account(self):
|
||||
|
||||
def exit_managed_account(self):
|
||||
self.is_managed = False
|
||||
if self._manager.profile_stack:
|
||||
fp, _, _ = self._manager.profile_stack.pop()
|
||||
self._manager.current_fingerprint = fp
|
||||
|
||||
# --- Secret Data ---
|
||||
def get_totp_code(self, entry_id): return "888999"
|
||||
def get_totp_secret(self, entry_id): return "AGENT_MOCK_SECRET"
|
||||
def get_seed_phrase(self, eid, parent): return "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||
def get_managed_account_seed(self, eid, parent): return self.get_seed_phrase(eid, parent)
|
||||
def get_clipboard_clear_delay(self): return 10
|
||||
def copy_to_clipboard(self, val): return True
|
||||
def generate_password(self, length, entry_id): return "MOCK_PASSWORD"
|
||||
def get_totp_code(self, entry_id):
|
||||
return "888999"
|
||||
|
||||
def get_totp_secret(self, entry_id):
|
||||
return "AGENT_MOCK_SECRET"
|
||||
|
||||
def get_seed_phrase(self, eid, parent):
|
||||
return "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||
|
||||
def get_managed_account_seed(self, eid, parent):
|
||||
return self.get_seed_phrase(eid, parent)
|
||||
|
||||
def get_clipboard_clear_delay(self):
|
||||
return 10
|
||||
|
||||
def copy_to_clipboard(self, val):
|
||||
return True
|
||||
|
||||
def generate_password(self, length, entry_id):
|
||||
return "MOCK_PASSWORD"
|
||||
|
||||
|
||||
async def run_full_walkthrough(v="v3"):
|
||||
service = AgentMockService()
|
||||
holder = {}
|
||||
|
||||
|
||||
def _hook(app):
|
||||
holder["app"] = app
|
||||
app.session_locked = False # Bypass lock
|
||||
app.session_locked = False # Bypass lock
|
||||
|
||||
launch_fn = launch_tui3 if v == "v3" else launch_tui2
|
||||
|
||||
|
||||
launch_fn(
|
||||
entry_service_factory=lambda: service,
|
||||
vault_service_factory=lambda: service,
|
||||
@@ -160,23 +217,23 @@ async def run_full_walkthrough(v="v3"):
|
||||
sync_service_factory=lambda: service,
|
||||
nostr_service_factory=lambda: service,
|
||||
config_service_factory=lambda: service,
|
||||
app_hook=_hook
|
||||
app_hook=_hook,
|
||||
)
|
||||
|
||||
|
||||
app = holder["app"]
|
||||
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
from textual.widgets import Static, Input, ListView, DataTable
|
||||
|
||||
print(f"--- TESTING TUI {v.upper()} ---")
|
||||
|
||||
|
||||
# 1. Entry Management
|
||||
print("PHASE 1: Entry Creation & Grid")
|
||||
service.add_entry("Gmail", 20)
|
||||
service.add_totp("Bank")
|
||||
service.add_entry("SubUser", 10, kind="managed_account")
|
||||
service.add_entry("MySeed", 24, kind="seed")
|
||||
|
||||
|
||||
await pilot.pause()
|
||||
print(f" [OK] Created {len(service.entries)} entries.")
|
||||
|
||||
@@ -185,7 +242,7 @@ async def run_full_walkthrough(v="v3"):
|
||||
# Real selection via DataTable
|
||||
table = app.screen.query_one("#entry-data-table", DataTable)
|
||||
table.focus()
|
||||
await pilot.press("down") # Select first row
|
||||
await pilot.press("down") # Select first row
|
||||
await pilot.press("enter")
|
||||
else:
|
||||
app._show_entry(1)
|
||||
@@ -196,19 +253,25 @@ async def run_full_walkthrough(v="v3"):
|
||||
print("PHASE 2: Secure Data (v/g)")
|
||||
if v == "v3":
|
||||
# Jump to MySeed (#4)
|
||||
app.selected_entry_id = 4
|
||||
app.selected_entry_id = 4
|
||||
await pilot.pause(0.5)
|
||||
# First press
|
||||
await pilot.press('v')
|
||||
await pilot.press("v")
|
||||
await pilot.pause(0.5)
|
||||
board_text = str(app.screen.query_one("#board-container").children[0].render())
|
||||
board_text = str(
|
||||
app.screen.query_one("#board-container").children[0].render()
|
||||
)
|
||||
print(f" [DEBUG] Board text after first 'v': {board_text[:100]}...")
|
||||
print(f" [OK] Seed Confirmation shown: {'CONFIRMATION' in board_text}")
|
||||
# Second press
|
||||
await pilot.press('v')
|
||||
await pilot.press("v")
|
||||
await pilot.pause(0.5)
|
||||
board_text_revealed = str(app.screen.query_one("#board-container").children[0].render())
|
||||
print(f" [DEBUG] Board text after second 'v': {board_text_revealed[:100]}...")
|
||||
board_text_revealed = str(
|
||||
app.screen.query_one("#board-container").children[0].render()
|
||||
)
|
||||
print(
|
||||
f" [DEBUG] Board text after second 'v': {board_text_revealed[:100]}..."
|
||||
)
|
||||
print(f" [OK] Seed Revealed: {'abandon' in board_text_revealed}")
|
||||
else:
|
||||
# v2 legacy test path
|
||||
@@ -217,19 +280,23 @@ async def run_full_walkthrough(v="v3"):
|
||||
# 3. Full-screen UX (V3 only)
|
||||
if v == "v3":
|
||||
print("PHASE 3: Full-screen screens")
|
||||
await pilot.press('shift+s')
|
||||
await pilot.press("shift+s")
|
||||
await pilot.pause(0.5)
|
||||
print(f" [OK] Settings Screen: {app.screen.__class__.__name__.endswith('SettingsScreen')}")
|
||||
await pilot.press('escape')
|
||||
print(
|
||||
f" [OK] Settings Screen: {app.screen.__class__.__name__.endswith('SettingsScreen')}"
|
||||
)
|
||||
await pilot.press("escape")
|
||||
await pilot.pause(0.5)
|
||||
|
||||
|
||||
app.selected_entry_id = 1
|
||||
await pilot.pause(0.5)
|
||||
await pilot.press('z')
|
||||
await pilot.press("z")
|
||||
await pilot.pause(0.5)
|
||||
print(f" [DEBUG] Screen after 'z': {app.screen.__class__.__name__}")
|
||||
print(f" [OK] Maximize Screen: {app.screen.__class__.__name__.endswith('MaximizedInspectorScreen')}")
|
||||
await pilot.press('escape')
|
||||
print(
|
||||
f" [OK] Maximize Screen: {app.screen.__class__.__name__.endswith('MaximizedInspectorScreen')}"
|
||||
)
|
||||
await pilot.press("escape")
|
||||
await pilot.pause(0.5)
|
||||
|
||||
# 4. Action Logic (Archive/Copy)
|
||||
@@ -237,11 +304,11 @@ async def run_full_walkthrough(v="v3"):
|
||||
if v == "v3":
|
||||
app.selected_entry_id = 1
|
||||
await pilot.pause(0.5)
|
||||
await pilot.press('a')
|
||||
await pilot.press("a")
|
||||
await pilot.pause(0.5)
|
||||
print(f" [DEBUG] Entry 1 archived in service: {1 in service.archived}")
|
||||
print(f" [OK] Archive: {1 in service.archived}")
|
||||
await pilot.press('c')
|
||||
await pilot.press("c")
|
||||
await pilot.pause(0.5)
|
||||
print(f" [OK] Copy action triggered.")
|
||||
|
||||
@@ -251,26 +318,30 @@ async def run_full_walkthrough(v="v3"):
|
||||
# Select managed account (ID 3)
|
||||
app.selected_entry_id = 3
|
||||
await pilot.pause(0.5)
|
||||
|
||||
|
||||
# Press 'e' to Edit
|
||||
await pilot.press('e')
|
||||
await pilot.press("e")
|
||||
await pilot.pause(0.5)
|
||||
print(f" [OK] Edit Screen opened: {app.screen.__class__.__name__.endswith('EditEntryScreen')}")
|
||||
await pilot.press('escape') # close edit screen
|
||||
print(
|
||||
f" [OK] Edit Screen opened: {app.screen.__class__.__name__.endswith('EditEntryScreen')}"
|
||||
)
|
||||
await pilot.press("escape") # close edit screen
|
||||
await pilot.pause(0.5)
|
||||
|
||||
|
||||
# Press 'm' to load profile
|
||||
await pilot.press('m')
|
||||
await pilot.press("m")
|
||||
await pilot.pause(0.5)
|
||||
print(f" [OK] Profile Loaded: {service.is_managed == True}")
|
||||
|
||||
|
||||
# Verify breadcrumb display
|
||||
brand_label = str(app.screen.query_one("#brand-fingerprint").render())
|
||||
print(f" [DEBUG] Breadcrumb text: {brand_label}")
|
||||
print(f" [OK] Sub-account breadcrumb structure visible: {'>' in brand_label and 'SUBACCT3' in brand_label}")
|
||||
|
||||
print(
|
||||
f" [OK] Sub-account breadcrumb structure visible: {'>' in brand_label and 'SUBACCT3' in brand_label}"
|
||||
)
|
||||
|
||||
# Press 'shift+m' to exit profile
|
||||
await pilot.press('shift+m')
|
||||
await pilot.press("shift+m")
|
||||
await pilot.pause(0.5)
|
||||
print(f" [OK] Profile Exited: {service.is_managed == False}")
|
||||
brand_label_exit = str(app.screen.query_one("#brand-fingerprint").render())
|
||||
@@ -279,31 +350,31 @@ async def run_full_walkthrough(v="v3"):
|
||||
# 6. Data I/O and Command Palette
|
||||
if v == "v3":
|
||||
print("PHASE 6: Data I/O and Commands")
|
||||
|
||||
|
||||
# Export selected entry
|
||||
app.selected_entry_id = 4 # Seed
|
||||
app.selected_entry_id = 4 # Seed
|
||||
await pilot.pause(0.5)
|
||||
await pilot.press('x')
|
||||
await pilot.press("x")
|
||||
await pilot.pause(0.5)
|
||||
print(f" [OK] Export Selected triggered.")
|
||||
|
||||
|
||||
# Test global commands via Command Palette
|
||||
app.action_open_palette()
|
||||
await pilot.pause(0.5)
|
||||
palette = app.screen.query_one("#palette")
|
||||
is_visible = palette.has_class("visible")
|
||||
print(f" [OK] Command Palette opened: {is_visible}")
|
||||
|
||||
|
||||
# Type and execute db-export
|
||||
await pilot.press(*list("db-export /tmp/test_export.zip"))
|
||||
await pilot.press('enter')
|
||||
await pilot.press("enter")
|
||||
await pilot.pause(0.5)
|
||||
print(f" [OK] db-export command executed.")
|
||||
|
||||
|
||||
app.action_open_palette()
|
||||
await pilot.pause(0.5)
|
||||
await pilot.press(*list("db-import /tmp/test_export.zip"))
|
||||
await pilot.press('enter')
|
||||
await pilot.press("enter")
|
||||
await pilot.pause(0.5)
|
||||
print(f" [OK] db-import command executed.")
|
||||
|
||||
@@ -317,14 +388,22 @@ async def run_full_walkthrough(v="v3"):
|
||||
else:
|
||||
print(" [FAIL] Missing click bindings in ActionBar.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# Verify BoardContainer styling for overflow (scrollability)
|
||||
board_css = app.screen.query_one("#board-container").styles
|
||||
if getattr(board_css, "overflow_y", None) == "auto" or getattr(board_css.overflow, "y", None) == "auto" or str(getattr(board_css, "overflow", "")).find("auto") != -1:
|
||||
print(" [OK] BoardContainer mapped with overflow: auto for scrollable bounds.")
|
||||
if (
|
||||
getattr(board_css, "overflow_y", None) == "auto"
|
||||
or getattr(board_css.overflow, "y", None) == "auto"
|
||||
or str(getattr(board_css, "overflow", "")).find("auto") != -1
|
||||
):
|
||||
print(
|
||||
" [OK] BoardContainer mapped with overflow: auto for scrollable bounds."
|
||||
)
|
||||
else:
|
||||
print(" [OK] BoardContainer CSS verified for container bounds. Overflow might be handled differently.")
|
||||
|
||||
print(
|
||||
" [OK] BoardContainer CSS verified for container bounds. Overflow might be handled differently."
|
||||
)
|
||||
|
||||
# Verify Board buttons contain click bindings
|
||||
has_clicks = False
|
||||
for btn in app.screen.query(".action-btn"):
|
||||
@@ -340,9 +419,10 @@ async def run_full_walkthrough(v="v3"):
|
||||
print(f"\n--- TUI {v.upper()} WALKTHROUGH COMPLETE ---")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--version", choices=["v2", "v3"], default="v3")
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
asyncio.run(run_full_walkthrough(v=args.version))
|
||||
@@ -3,7 +3,9 @@ set -eo pipefail
|
||||
|
||||
mkdir -p artifacts/coverage
|
||||
|
||||
./scripts/tui2_check_smoke.sh
|
||||
if [[ "${TUI2_SMOKE_GATE:-0}" == "1" ]]; then
|
||||
./scripts/tui2_check_smoke.sh
|
||||
fi
|
||||
|
||||
if [[ -x ".venv/bin/python" ]]; then
|
||||
py_bin=".venv/bin/python"
|
||||
@@ -61,7 +63,10 @@ if [[ $status -eq 0 && "${CRITICAL_COVERAGE_GATE:-1}" == "1" ]]; then
|
||||
artifacts/coverage/coverage.json \
|
||||
--json-output artifacts/coverage/critical_gate.full.json
|
||||
fi
|
||||
if [[ $status -eq 0 && "${TUI2_COVERAGE_GATE:-1}" == "1" ]]; then
|
||||
if [[ $status -eq 0 && "${TUI2_COVERAGE_GATE:-0}" == "1" ]]; then
|
||||
./scripts/tui2_coverage_gate.sh
|
||||
fi
|
||||
if [[ $status -eq 0 && "${TUI3_COVERAGE_GATE:-1}" == "1" ]]; then
|
||||
./scripts/tui3_coverage_gate.sh
|
||||
fi
|
||||
exit $status
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
mkdir -p artifacts/coverage
|
||||
|
||||
if [[ -x ".venv/bin/python" ]]; then
|
||||
py_bin=".venv/bin/python"
|
||||
elif command -v python >/dev/null 2>&1; then
|
||||
py_bin="python"
|
||||
else
|
||||
py_bin="python3"
|
||||
fi
|
||||
|
||||
if ! "$py_bin" - <<'PY' >/dev/null 2>&1
|
||||
import importlib.util
|
||||
raise SystemExit(0 if importlib.util.find_spec("textual") else 1)
|
||||
PY
|
||||
then
|
||||
if [[ "${REQUIRE_TEXTUAL:-0}" == "1" ]]; then
|
||||
echo "Error: textual runtime is unavailable; cannot run TUI v3 coverage gate." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "tui3 coverage gate: skipped (textual runtime unavailable)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
"$py_bin" -m pytest -q \
|
||||
--cov=src \
|
||||
--cov-report=json:artifacts/coverage/coverage.tui3_gate.json \
|
||||
src/tests/test_tui_v3_parity.py \
|
||||
src/tests/test_tui_v3_smoke.py
|
||||
|
||||
"$py_bin" scripts/check_critical_coverage.py \
|
||||
artifacts/coverage/coverage.tui3_gate.json \
|
||||
--no-default-thresholds \
|
||||
--threshold src/seedpass/tui_v3/app.py=35 \
|
||||
--json-output artifacts/coverage/critical_gate.tui3.json
|
||||
|
||||
echo "tui3 coverage gate: ok"
|
||||
Reference in new issue
Block a user