diff --git a/scripts/canonical_json_cases.py b/scripts/canonical_json_cases.py index 21e50ea7..767f6047 100644 --- a/scripts/canonical_json_cases.py +++ b/scripts/canonical_json_cases.py @@ -29,7 +29,7 @@ CASES: list = [ {"\U0001f600": 1, "!": 2}, {"z": 1, "é": 2, "中": 3, "\U0001f600": 4}, # Escaping - {"quote\"": "back\\slash"}, + {'quote"': "back\\slash"}, {"ctrl": "\x00\x01\x1f"}, {"nl": "line\nbreak\ttab"}, {"unicode": "café 中文 \U0001f512"}, diff --git a/scripts/cross_impl_check.py b/scripts/cross_impl_check.py index 6c13176a..a8759dcd 100644 --- a/scripts/cross_impl_check.py +++ b/scripts/cross_impl_check.py @@ -38,16 +38,12 @@ sys.path.insert(0, str(REPO / "src")) CLI_BIN = REPO / "js" / "packages" / "cli" / "bin" / "seedpass-js.mjs" # Throwaway seeds. Never used for real funds or real vaults. -SEED_A = ( - "rival cover produce defy coconut arrow empty acid crime cereal strong icon" -) +SEED_A = "rival cover produce defy coconut arrow empty acid crime cereal strong icon" SEED_B = ( "romance waste exercise alone pistol mushroom aunt series weasel muscle move skirt" ) # Valid words, deliberately wrong checksum: both implementations must refuse it. -SEED_INVALID = ( - "gaze stereo trend brown chunk hero pole width once tent lift bird" -) +SEED_INVALID = "gaze stereo trend brown chunk hero pole width once tent lift bird" PASSWORD = "cross-impl-check-password" results: list[tuple[str, bool, str]] = [] @@ -273,10 +269,14 @@ def py_secrets_for(index: dict, seed: str) -> dict[str, str]: out[label] = entry["content"] elif kind == "seed": out[label] = derive_seed_phrase( - bip85, int(entry.get("index", int(idx))), int(entry.get("word_count", 24)) + bip85, + int(entry.get("index", int(idx))), + int(entry.get("word_count", 24)), ) elif kind == "managed_account": - out[label] = derive_seed_phrase(bip85, int(entry.get("index", int(idx))), 12) + out[label] = derive_seed_phrase( + bip85, int(entry.get("index", int(idx))), 12 + ) elif kind == "pgp": from seedpass.core.password_generation import derive_pgp_key @@ -326,16 +326,16 @@ def phase_a(tmp: Path) -> None: fp = py_create_profile(app_dir, SEED_A, PASSWORD) try: - listed = json.loads(run_cli(app_dir, "entry", "list", env_extra={"SEEDPASS_MNEMONIC": SEED_A})) + listed = json.loads( + run_cli(app_dir, "entry", "list", env_extra={"SEEDPASS_MNEMONIC": SEED_A}) + ) check("TS opens the Python-created index", True) except Exception as exc: check("TS opens the Python-created index", False, str(exc)) return py_seed, py_index = py_open_profile(app_dir, fp, PASSWORD) - py_labels = sorted( - e["label"] for e in py_index.get("entries", {}).values() - ) + py_labels = sorted(e["label"] for e in py_index.get("entries", {}).values()) ts_labels = sorted(row["label"] for row in listed) check("entry sets match", py_labels == ts_labels, f"{py_labels} != {ts_labels}") @@ -359,7 +359,11 @@ def phase_a(tmp: Path) -> None: with agent_running(app_dir): try: run_cli( - app_dir, "vault", "unlock", "--ttl", "5", + app_dir, + "vault", + "unlock", + "--ttl", + "5", env_extra={"SEEDPASS_PASSWORD": PASSWORD}, ) revealed = run_cli(app_dir, "entry", "reveal", "python-api") @@ -370,7 +374,9 @@ def phase_a(tmp: Path) -> None: ) except Exception as exc: check( - "TS unlocks the Python profile with the master password", False, str(exc) + "TS unlocks the Python profile with the master password", + False, + str(exc), ) @@ -379,25 +385,64 @@ def phase_b(tmp: Path) -> None: app_dir = tmp / "b" app_dir.mkdir() run_cli( - app_dir, "fingerprint", "add", "--name", "ts-made", + app_dir, + "fingerprint", + "add", + "--name", + "ts-made", env_extra={"SEEDPASS_MNEMONIC": SEED_B, "SEEDPASS_PASSWORD": PASSWORD}, ) env = {"SEEDPASS_MNEMONIC": SEED_B} - run_cli(app_dir, "entry", "add", "password", "ts-site.example", "--length", "22", env_extra=env) + run_cli( + app_dir, + "entry", + "add", + "password", + "ts-site.example", + "--length", + "22", + env_extra=env, + ) run_cli(app_dir, "entry", "add", "totp", "ts-totp", env_extra=env) - run_cli(app_dir, "entry", "add", "key-value", "ts-api", "token", "ts-secret-value", env_extra=env) + run_cli( + app_dir, + "entry", + "add", + "key-value", + "ts-api", + "token", + "ts-secret-value", + env_extra=env, + ) run_cli(app_dir, "entry", "add", "managed-account", "ts-managed", env_extra=env) run_cli(app_dir, "entry", "add", "ssh", "ts-ssh", env_extra=env) run_cli(app_dir, "entry", "add", "nostr", "ts-nostr", env_extra=env) run_cli( - app_dir, "entry", "add", "pgp", "ts-pgp", "--user-id", "ts@example.com", + app_dir, + "entry", + "add", + "pgp", + "ts-pgp", + "--user-id", + "ts@example.com", env_extra=env, ) - run_cli(app_dir, "entry", "add", "document", "ts-doc", "ts document body", env_extra=env) + run_cli( + app_dir, "entry", "add", "document", "ts-doc", "ts document body", env_extra=env + ) run_cli(app_dir, "entry", "add", "seed", "ts-seed", "--words", "24", env_extra=env) run_cli( - app_dir, "entry", "add", "totp", "ts-totp-imported", - "--secret", "JBSWY3DPEHPK3PXP", "--period", "45", "--digits", "8", + app_dir, + "entry", + "add", + "totp", + "ts-totp-imported", + "--secret", + "JBSWY3DPEHPK3PXP", + "--period", + "45", + "--digits", + "8", env_extra=env, ) @@ -422,7 +467,9 @@ def phase_b(tmp: Path) -> None: for k in py_secrets if py_secrets[k].rstrip("\n") != (ts_secrets.get(k) or "").rstrip("\n") ] - check("every secret derives identically", not mismatches, f"mismatched: {mismatches}") + check( + "every secret derives identically", not mismatches, f"mismatched: {mismatches}" + ) # Python's schema validation should accept the TS-written index from seedpass.core.migrations import LATEST_VERSION # type: ignore @@ -452,11 +499,17 @@ def phase_c(tmp: Path) -> None: vault = Vault(enc_mgr, fp_dir) cfg_mgr = ConfigManager(vault, fp_dir) backup_mgr = BackupManager(fp_dir, cfg_mgr) - py_export = export_backup(vault, backup_mgr, fp_dir / "py-export.json", parent_seed=SEED_A) + py_export = export_backup( + vault, backup_mgr, fp_dir / "py-export.json", parent_seed=SEED_A + ) try: summary = json.loads( run_cli( - app_a, "vault", "import", str(py_export), "--inspect", + app_a, + "vault", + "import", + str(py_export), + "--inspect", env_extra={"SEEDPASS_MNEMONIC": SEED_A}, ) ) @@ -473,7 +526,10 @@ def phase_c(tmp: Path) -> None: fp_b = generate_fingerprint(SEED_B) ts_export = app_b / "ts-export.json" run_cli( - app_b, "vault", "export", str(ts_export), + app_b, + "vault", + "export", + str(ts_export), env_extra={"SEEDPASS_MNEMONIC": SEED_B}, ) fp_dir_b = app_b / fp_b @@ -481,6 +537,7 @@ def phase_c(tmp: Path) -> None: vault_b = Vault(enc_b, fp_dir_b) cfg_b = ConfigManager(vault_b, fp_dir_b) backup_b = BackupManager(fp_dir_b, cfg_b) + def user_data(index: dict) -> str: """User-visible state only. @@ -504,7 +561,9 @@ def phase_c(tmp: Path) -> None: "user data changed across the round trip", ) except Exception as exc: - check("Python imports a TS portable backup (checksum verified)", False, str(exc)) + check( + "Python imports a TS portable backup (checksum verified)", False, str(exc) + ) def phase_d(tmp: Path) -> None: @@ -526,8 +585,15 @@ def phase_d(tmp: Path) -> None: detail = "" try: run_cli( - app_dir, "fingerprint", "add", "--name", "should-not-exist", - env_extra={"SEEDPASS_MNEMONIC": SEED_INVALID, "SEEDPASS_PASSWORD": PASSWORD}, + app_dir, + "fingerprint", + "add", + "--name", + "should-not-exist", + env_extra={ + "SEEDPASS_MNEMONIC": SEED_INVALID, + "SEEDPASS_PASSWORD": PASSWORD, + }, ) detail = "TS created a profile from an invalid phrase" except Exception as exc: @@ -593,7 +659,9 @@ def phase_e(tmp: Path) -> None: check("Python and TS agree on the migrated entry set", py_labels == labels) # And TS must not have written a downgraded index back to disk - reloaded = Vault(EncryptionManager(derive_index_key(SEED_A), fp_dir), fp_dir).load_index() + reloaded = Vault( + EncryptionManager(derive_index_key(SEED_A), fp_dir), fp_dir + ).load_index() check( "Python still reads the profile after TS touched it", sorted(e["label"] for e in reloaded.get("entries", {}).values()) == labels, @@ -686,9 +754,15 @@ def phase_f(tmp: Path) -> None: encrypted = vault.get_encrypted_index() manifest, manifest_id = asyncio.run(client.publish_snapshot(encrypted)) published = bool(manifest_id) and bool(manifest.chunks) - check("Python publishes a snapshot to the relay", published, str(manifest_id)) + check( + "Python publishes a snapshot to the relay", published, str(manifest_id) + ) except Exception as exc: - check("Python publishes a snapshot to the relay", False, f"{type(exc).__name__}: {exc}") + check( + "Python publishes a snapshot to the relay", + False, + f"{type(exc).__name__}: {exc}", + ) return # --- TypeScript restores from what Python published ------------------- @@ -714,7 +788,9 @@ def phase_f(tmp: Path) -> None: f"{ts_labels} != {py_labels}", ) revealed = run_cli(ts_dir, "entry", "reveal", "python-api", env_extra=env) - check("restored secrets are intact", revealed == "py-secret-value", revealed) + check( + "restored secrets are intact", revealed == "py-secret-value", revealed + ) except Exception as exc: check("TS restores the Python-published snapshot", False, str(exc)) @@ -757,7 +833,9 @@ def phase_g(tmp: Path) -> None: links = json.loads(run_cli(app_dir, "entry", "links", "0", env_extra=env)) check( "TS sees Python's link with a resolved target", - len(links) == 1 and links[0]["relation"] == "related_to" and links[0]["target_id"] == 2, + len(links) == 1 + and links[0]["relation"] == "related_to" + and links[0]["target_id"] == 2, json.dumps(links), ) archived = json.loads(run_cli(app_dir, "entry", "get", "1", env_extra=env)) @@ -770,7 +848,11 @@ def phase_g(tmp: Path) -> None: _vault2, em2, _cfg2 = _py_vault(app_dir, fp, SEED_A) entry0 = em2.retrieve_entry(0) entry1 = em2.retrieve_entry(1) - check("Python sees TS's edit", entry0.get("notes") == "edited-by-ts", str(entry0.get("notes"))) + check( + "Python sees TS's edit", + entry0.get("notes") == "edited-by-ts", + str(entry0.get("notes")), + ) check("Python sees TS's unarchive", entry1.get("archived") is False) check("Python sees TS's link removal", em2.get_links(0) == []) @@ -786,14 +868,25 @@ def phase_h(tmp: Path) -> None: "schema_version": 4, "entries": { "0": { - "type": "password", "kind": "password", "label": "shared", - "length": 16, "archived": False, "notes": "from-current", - "tags": ["a"], "modified_ts": base_ts, + "type": "password", + "kind": "password", + "label": "shared", + "length": 16, + "archived": False, + "notes": "from-current", + "tags": ["a"], + "modified_ts": base_ts, }, "1": { - "type": "key_value", "kind": "key_value", "label": "only-current", - "key": "k", "value": "v", "archived": False, "notes": "", - "tags": [], "modified_ts": base_ts, + "type": "key_value", + "kind": "key_value", + "label": "only-current", + "key": "k", + "value": "v", + "archived": False, + "notes": "", + "tags": [], + "modified_ts": base_ts, }, }, } @@ -801,20 +894,33 @@ def phase_h(tmp: Path) -> None: "schema_version": 4, "entries": { "0": { - "type": "password", "kind": "password", "label": "shared", - "length": 16, "archived": True, "notes": "", - "tags": ["b"], "modified_ts": base_ts, "username": "incoming-user", + "type": "password", + "kind": "password", + "label": "shared", + "length": 16, + "archived": True, + "notes": "", + "tags": ["b"], + "modified_ts": base_ts, + "username": "incoming-user", }, "2": { - "type": "password", "kind": "password", "label": "only-incoming", - "length": 20, "archived": False, "notes": "", "tags": [], + "type": "password", + "kind": "password", + "label": "only-incoming", + "length": 20, + "archived": False, + "notes": "", + "tags": [], "modified_ts": base_ts + 5, }, }, } py_merged = merge_index_payloads( - json.loads(json.dumps(current)), json.loads(json.dumps(incoming)), source_tag="xtest" + json.loads(json.dumps(current)), + json.loads(json.dumps(incoming)), + source_tag="xtest", ) # Run the TS merge through a tiny node harness against the same inputs @@ -829,7 +935,9 @@ def phase_h(tmp: Path) -> None: try: proc = subprocess.run( ["node", str(script), json.dumps([current, incoming])], - capture_output=True, text=True, cwd=str(CLI_BIN.parent), + capture_output=True, + text=True, + cwd=str(CLI_BIN.parent), ) if proc.returncode != 0: check("TS and Python merge identically", False, proc.stderr.strip()[:200]) @@ -880,7 +988,11 @@ def phase_i(tmp: Path) -> None: with agent_running(app_dir): try: run_cli( - app_dir, "vault", "unlock", "--ttl", "10", + app_dir, + "vault", + "unlock", + "--ttl", + "10", env_extra={"SEEDPASS_PASSWORD": PASSWORD}, ) revealed = run_cli(app_dir, "entry", "reveal", "python-api") @@ -937,8 +1049,26 @@ def phase_k(tmp: Path) -> None: # Simulate a migration window: the user works exclusively in TS for a # while, exercising creation, modification and archival. - run_cli(app_dir, "entry", "add", "password", "post-migration-site", "--length", "22", env_extra=env) - run_cli(app_dir, "entry", "add", "key-value", "post-migration-kv", "k", "rollback-value", env_extra=env) + run_cli( + app_dir, + "entry", + "add", + "password", + "post-migration-site", + "--length", + "22", + env_extra=env, + ) + run_cli( + app_dir, + "entry", + "add", + "key-value", + "post-migration-kv", + "k", + "rollback-value", + env_extra=env, + ) run_cli(app_dir, "entry", "add", "totp", "post-migration-totp", env_extra=env) run_cli(app_dir, "entry", "modify", "0", "--notes", "touched by ts", env_extra=env) run_cli(app_dir, "entry", "archive", "1", env_extra=env) @@ -966,12 +1096,13 @@ def phase_k(tmp: Path) -> None: # And Python can still derive the secrets for what TS created. py_secrets = py_secrets_for(py_index, SEED_A) - ts_secrets = ts_secrets_for(app_dir, ["post-migration-site", "post-migration-kv"], SEED_A) + ts_secrets = ts_secrets_for( + app_dir, ["post-migration-site", "post-migration-kv"], SEED_A + ) check( "secrets for TS-created entries match in Python", all( - py_secrets[k].rstrip("\n") == ts_secrets[k].rstrip("\n") - for k in ts_secrets + py_secrets[k].rstrip("\n") == ts_secrets[k].rstrip("\n") for k in ts_secrets ), ) @@ -1007,7 +1138,11 @@ def phase_l(tmp: Path) -> None: "blacklisted": "foreign meaning, not our archive flag", "website": "foreign meaning, not our label alias", "words": ["foreign", "list"], - "bitlogin": {"admins": ["npub1aaaa"], "roles": {"sales": ["npub1bbbb"]}, "policy_rev": 7}, + "bitlogin": { + "admins": ["npub1aaaa"], + "roles": {"sales": ["npub1bbbb"]}, + "policy_rev": 7, + }, } foreign_top_level = {"spec": "bitlogin-v1", "org_count": 1} diff --git a/scripts/differential_fuzz.py b/scripts/differential_fuzz.py index 53d4179d..b2056e40 100644 --- a/scripts/differential_fuzz.py +++ b/scripts/differential_fuzz.py @@ -110,7 +110,8 @@ def rand_text(rng: random.Random) -> str: if rng.random() < 0.5: return rng.choice(INTERESTING_TEXT) return "".join( - rng.choice(string.ascii_letters + string.digits + " -_.") for _ in range(rng.randint(0, 12)) + rng.choice(string.ascii_letters + string.digits + " -_.") + for _ in range(rng.randint(0, 12)) ) @@ -262,7 +263,9 @@ def case_password(rng: random.Random) -> tuple[str, Any, dict[str, Any]]: index = rng.randint(0, 5000) version = rng.choice([1, 2]) bip85 = BIP85(Bip39SeedGenerator(mnemonic).Generate("")) - pg = PasswordGenerator(_Deriver(), mnemonic, bip85, policy=PasswordPolicy(**policy_kwargs)) + pg = PasswordGenerator( + _Deriver(), mnemonic, bip85, policy=PasswordPolicy(**policy_kwargs) + ) kwargs = {} if version == 1 else {"gen_version": version} try: result: Any = pg.generate_password(length=length, index=index, **kwargs) @@ -302,7 +305,9 @@ def case_merge(rng: random.Random) -> tuple[str, Any, dict[str, Any]]: def case_index0(rng: random.Random) -> tuple[str, Any, dict[str, Any]]: from seedpass.core import index0 as index0_mod - fp_dir = f"/tmp/seedpass/{''.join(rng.choice('0123456789ABCDEF') for _ in range(16))}" + fp_dir = ( + f"/tmp/seedpass/{''.join(rng.choice('0123456789ABCDEF') for _ in range(16))}" + ) payload: dict[str, Any] = {"schema_version": 4, "entries": rand_entries(rng)} events = [] # Same reasoning as the tombstone cap: occasionally produce enough events, @@ -313,7 +318,9 @@ def case_index0(rng: random.Random) -> tuple[str, Any, dict[str, Any]]: # peaked at 63 distinct subjects across 250 cases -- one short -- so the # eviction path stayed unreachable and a deliberate off-by-one there went # undetected. Bounded code has to be driven past its bound, with margin. - event_count = rng.randint(SUBJECT_CAP + 20, SUBJECT_CAP + 60) if big else rng.randint(0, 5) + event_count = ( + rng.randint(SUBJECT_CAP + 20, SUBJECT_CAP + 60) if big else rng.randint(0, 5) + ) # A large run is clustered into one day half the time, so both the subject # cap (per checkpoint) and the checkpoint retention limit get reached. day_spread = not big or rng.random() < 0.5 @@ -359,7 +366,12 @@ def case_index0(rng: random.Random) -> tuple[str, Any, dict[str, Any]]: return ( "index0", result, - {"op": "index0", "fingerprintDir": fp_dir, "entries": payload.get("entries", {}), "events": events}, + { + "op": "index0", + "fingerprintDir": fp_dir, + "entries": payload.get("entries", {}), + "events": events, + }, ) @@ -397,7 +409,9 @@ def case_semantic(rng: random.Random) -> tuple[str, Any, dict[str, Any]]: try: index.build(entries) records = json.loads( - (Path(tmpdir) / "semantic_index" / "records.json").read_text(encoding="utf-8") + (Path(tmpdir) / "semantic_index" / "records.json").read_text( + encoding="utf-8" + ) ) hits = index.search(query, k=10) except Exception as exc: @@ -502,7 +516,9 @@ def run_ts(requests: list[dict[str, Any]]) -> list[Any]: def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--cases", type=int, default=200, help="cases per surface") - parser.add_argument("--seed", type=int, default=1, help="RNG seed (reproduces a run)") + parser.add_argument( + "--seed", type=int, default=1, help="RNG seed (reproduces a run)" + ) parser.add_argument( "--only", action="append", @@ -514,7 +530,9 @@ def main() -> int: surfaces = args.only or sorted(BUILDERS) rng = random.Random(args.seed) - print(f"differential fuzz: seed={args.seed} cases={args.cases} surfaces={','.join(surfaces)}") + print( + f"differential fuzz: seed={args.seed} cases={args.cases} surfaces={','.join(surfaces)}" + ) expected: list[tuple[str, int, Any]] = [] requests: list[dict[str, Any]] = [] @@ -528,12 +546,16 @@ def main() -> int: print(f" generated {len(requests)} cases; running the TypeScript side...") ts_results = run_ts(requests) if len(ts_results) != len(expected): - raise SystemExit(f"runner returned {len(ts_results)} results for {len(expected)} cases") + raise SystemExit( + f"runner returned {len(ts_results)} results for {len(expected)} cases" + ) divergences: list[tuple[str, int, str, str, dict[str, Any]]] = [] known: list[tuple[str, int]] = [] skipped = 0 - for (name, idx, python_result), ts_result, request in zip(expected, ts_results, requests): + for (name, idx, python_result), ts_result, request in zip( + expected, ts_results, requests + ): if request.get("op") == "noop": skipped += 1 continue @@ -552,7 +574,9 @@ def main() -> int: for surface in sorted(by_surface): failures = sum(1 for d in divergences if d[0] == surface) mark = "PASS" if failures == 0 else "FAIL" - print(f" [{mark}] {surface}: {by_surface[surface] - failures}/{by_surface[surface]} agree") + print( + f" [{mark}] {surface}: {by_surface[surface] - failures}/{by_surface[surface]} agree" + ) if skipped: print(f" ({skipped} cases skipped: the Python side refused the input)") @@ -567,12 +591,21 @@ def main() -> int: ) if divergences: - print(f"\n{len(divergences)} DIVERGENCE(S) — reproduce with --seed {args.seed}\n") - dump = Path("/tmp/claude-1000/-home-user/4efd790e-427f-430a-b379-ef8391e6744f/scratchpad/divergences.json") - dump.write_text(json.dumps([ - {"surface": n, "case": i, "python": p_, "ts": t, "input": r} - for n, i, p_, t, r in divergences - ], indent=1)) + print( + f"\n{len(divergences)} DIVERGENCE(S) — reproduce with --seed {args.seed}\n" + ) + dump = Path( + "/tmp/claude-1000/-home-user/4efd790e-427f-430a-b379-ef8391e6744f/scratchpad/divergences.json" + ) + dump.write_text( + json.dumps( + [ + {"surface": n, "case": i, "python": p_, "ts": t, "input": r} + for n, i, p_, t, r in divergences + ], + indent=1, + ) + ) print(f" (full list written to {dump})\n") for name, idx, py_canon, ts_canon, request in divergences[:5]: print(f"--- {name} case {idx} ---") diff --git a/scripts/generate_test_profile.py b/scripts/generate_test_profile.py index 104a6bab..7e830bbf 100644 --- a/scripts/generate_test_profile.py +++ b/scripts/generate_test_profile.py @@ -45,6 +45,8 @@ def test_app_dir() -> Path: another ``tests`` segment on each reload. """ return Path(consts.APP_DIR) / "tests" + + from utils.key_derivation import derive_key_from_password, derive_index_key from seedpass.core.encryption import EncryptionManager from seedpass.core.vault import Vault diff --git a/scripts/generate_ts_port_fixtures.py b/scripts/generate_ts_port_fixtures.py index f7324d65..a59464e7 100644 --- a/scripts/generate_ts_port_fixtures.py +++ b/scripts/generate_ts_port_fixtures.py @@ -58,8 +58,7 @@ MNEMONICS = { "abandon abandon abandon abandon abandon about" ), "legal12": ( - "legal winner thank year wave sausage worth useful " - "legal winner thank yellow" + "legal winner thank year wave sausage worth useful " "legal winner thank yellow" ), "zoo24": ( "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo " @@ -326,7 +325,9 @@ def gen_fingerprints() -> dict: { "mnemonic_id": "abandon12-mixed-case-padded", "mnemonic": " " + MNEMONICS[PRIMARY].upper() + " ", - "fingerprint": generate_fingerprint(" " + MNEMONICS[PRIMARY].upper() + " "), + "fingerprint": generate_fingerprint( + " " + MNEMONICS[PRIMARY].upper() + " " + ), } ) return { @@ -412,9 +413,7 @@ def _build_entries_index(post_add=None) -> dict: post_add(em) index = vault.load_index() - entries = { - k: v for k, v in index.items() if k != "_system" - } + entries = {k: v for k, v in index.items() if k != "_system"} return entries @@ -461,7 +460,11 @@ def gen_password_kdf() -> dict: fp = generate_fingerprint(MNEMONICS[PRIMARY]) pbkdf2_cases = [] - for password in ("fixture-password", "correct horse battery staple", " pässwörd "): + for password in ( + "fixture-password", + "correct horse battery staple", + " pässwörd ", + ): for iterations in (50_000, 100_000): key = derive_key_from_password(password, fp, iterations=iterations) pbkdf2_cases.append( @@ -534,8 +537,10 @@ def gen_legacy_payloads() -> dict: seed_key_b64 = derive_key_from_password("fixture-password", fp) seed_key = base64.urlsafe_b64decode(seed_key_b64) seed_nonce = hashlib.sha256(b"seedpass-ts-fixture-seed-nonce").digest()[:12] - seed_ct = b"V3|" + seed_nonce + AESGCM(seed_key).encrypt( - seed_nonce, mnemonic.encode(), None + seed_ct = ( + b"V3|" + + seed_nonce + + AESGCM(seed_key).encrypt(seed_nonce, mnemonic.encode(), None) ) kdf_dict = { "name": "pbkdf2-sha256", @@ -592,7 +597,9 @@ def gen_nostr_snapshot(vault_payload_b64: str) -> dict: limit = 800 manifest, chunks = prepare_snapshot(encrypted, limit) # Re-chunk the pinned compression so chunk bytes/hashes are deterministic - pinned_chunks = [compressed[i : i + limit] for i in range(0, len(compressed), limit)] + pinned_chunks = [ + compressed[i : i + limit] for i in range(0, len(compressed), limit) + ] metas = [ { "id": f"seedpass-chunk-{i:04d}", @@ -704,8 +711,12 @@ def gen_sync_merge() -> dict: "schema_version": 4, "entries": { "0": entry( - "site", t, notes="from-incoming", tags=["b", "a"], - username="alice", archived=True, + "site", + t, + notes="from-incoming", + tags=["b", "a"], + username="alice", + archived=True, ) }, }, @@ -713,7 +724,10 @@ def gen_sync_merge() -> dict: ), _merge_case( "incoming-delete-creates-tombstone", - {"schema_version": 4, "entries": {"0": entry("site", t), "1": entry("keep", t)}}, + { + "schema_version": 4, + "entries": {"0": entry("site", t), "1": entry("keep", t)}, + }, { "schema_version": 4, "entries": {"0": {**entry("site", t + 5), "_deleted": True}}, @@ -727,7 +741,12 @@ def gen_sync_merge() -> dict: "entries": {}, "_sync_meta": { "tombstones": { - "0": {"deleted_ts": t, "entry_hash": "", "event_hash": "", "source": "x"} + "0": { + "deleted_ts": t, + "entry_hash": "", + "event_hash": "", + "source": "x", + } } }, }, @@ -741,7 +760,12 @@ def gen_sync_merge() -> dict: "entries": {}, "_sync_meta": { "tombstones": { - "0": {"deleted_ts": t + 20, "entry_hash": "", "event_hash": "", "source": "x"} + "0": { + "deleted_ts": t + 20, + "entry_hash": "", + "event_hash": "", + "source": "x", + } } }, }, @@ -755,7 +779,12 @@ def gen_sync_merge() -> dict: "entries": {}, "_sync_meta": { "tombstones": { - "0": {"deleted_ts": t + 1, "entry_hash": "aa", "event_hash": "", "source": "one"} + "0": { + "deleted_ts": t + 1, + "entry_hash": "aa", + "event_hash": "", + "source": "one", + } } }, }, @@ -764,7 +793,12 @@ def gen_sync_merge() -> dict: "entries": {}, "_sync_meta": { "tombstones": { - "0": {"deleted_ts": t + 9, "entry_hash": "bb", "event_hash": "", "source": "two"} + "0": { + "deleted_ts": t + 9, + "entry_hash": "bb", + "event_hash": "", + "source": "two", + } } }, }, @@ -798,14 +832,25 @@ def gen_delta_replay() -> dict: def entry(label: str, ts: int) -> dict: return { - "type": "password", "kind": "password", "label": label, - "length": 16, "archived": False, "notes": "", "tags": [], + "type": "password", + "kind": "password", + "label": label, + "length": 16, + "archived": False, + "notes": "", + "tags": [], "modified_ts": ts, } snapshot_index = {"schema_version": 4, "entries": {"0": entry("base", t)}} - delta1 = {"schema_version": 4, "entries": {"0": entry("base-renamed", t + 10), "1": entry("added", t + 10)}} - delta2 = {"schema_version": 4, "entries": {"1": {**entry("added", t + 20), "_deleted": True}}} + delta1 = { + "schema_version": 4, + "entries": {"0": entry("base-renamed", t + 10), "1": entry("added", t + 10)}, + } + delta2 = { + "schema_version": 4, + "entries": {"1": {**entry("added", t + 20), "_deleted": True}}, + } def encrypt(payload: dict, nonce_seed: bytes) -> bytes: nonce = hashlib.sha256(nonce_seed).digest()[:12] @@ -1158,7 +1203,11 @@ def gen_pgp_keys() -> dict: cases = [] for mid in (PRIMARY, "zoo24"): bip85 = _bip85(MNEMONICS[mid]) - for index, user_id in ((0, "fixture@example.com"), (3, ""), (7, "Test User ")): + for index, user_id in ( + (0, "fixture@example.com"), + (3, ""), + (7, "Test User "), + ): priv, pub, fp = derive_pgp_key(bip85, index, "ed25519", user_id) cases.append( { @@ -1300,26 +1349,85 @@ def gen_semantic() -> dict: from seedpass.core.semantic_index import SemanticIndex entries = [ - {"id": 0, "kind": "password", "label": "first-ever.example", - "username": "zero", "notes": "the very first entry a profile creates", - "tags": ["edge"]}, - {"id": 1, "kind": "password", "label": "bank.example", "username": "alice", - "url": "https://bank.example", "notes": "main current account", - "tags": ["money", "daily"]}, - {"id": 2, "kind": "key_value", "label": "deploy-token", "key": "DEPLOY_TOKEN", - "value": "SECRET-MUST-NOT-APPEAR", "notes": "ci pipeline", "tags": ["ops"]}, - {"id": 3, "kind": "document", "label": "recovery notes", - "content": "how to recover the bank account if locked out", "tags": ["docs"]}, - {"id": 4, "kind": "totp", "label": "email-2fa", "issuer": "Fastmail", - "notes": "", "tags": ["email"]}, - {"id": 5, "kind": "ssh", "label": "prod-server", "fingerprint": "SHA256:abcdef", - "notes": "production access", "tags": ["ops"]}, - {"id": 6, "kind": "nostr", "label": "social", "npub": "npub1example", - "notes": "", "tags": []}, - {"id": 7, "kind": "seed", "label": "child seed", "notes": "not an indexed kind"}, - {"id": 8, "kind": "document", "label": "", "content": "", "notes": "", "tags": []}, - {"id": 9, "kind": "password", "label": "linked", "notes": "", "tags": [], - "links": [{"relation": "depends_on", "note": "see recovery notes"}]}, + { + "id": 0, + "kind": "password", + "label": "first-ever.example", + "username": "zero", + "notes": "the very first entry a profile creates", + "tags": ["edge"], + }, + { + "id": 1, + "kind": "password", + "label": "bank.example", + "username": "alice", + "url": "https://bank.example", + "notes": "main current account", + "tags": ["money", "daily"], + }, + { + "id": 2, + "kind": "key_value", + "label": "deploy-token", + "key": "DEPLOY_TOKEN", + "value": "SECRET-MUST-NOT-APPEAR", + "notes": "ci pipeline", + "tags": ["ops"], + }, + { + "id": 3, + "kind": "document", + "label": "recovery notes", + "content": "how to recover the bank account if locked out", + "tags": ["docs"], + }, + { + "id": 4, + "kind": "totp", + "label": "email-2fa", + "issuer": "Fastmail", + "notes": "", + "tags": ["email"], + }, + { + "id": 5, + "kind": "ssh", + "label": "prod-server", + "fingerprint": "SHA256:abcdef", + "notes": "production access", + "tags": ["ops"], + }, + { + "id": 6, + "kind": "nostr", + "label": "social", + "npub": "npub1example", + "notes": "", + "tags": [], + }, + { + "id": 7, + "kind": "seed", + "label": "child seed", + "notes": "not an indexed kind", + }, + { + "id": 8, + "kind": "document", + "label": "", + "content": "", + "notes": "", + "tags": [], + }, + { + "id": 9, + "kind": "password", + "label": "linked", + "notes": "", + "tags": [], + "links": [{"relation": "depends_on", "note": "see recovery notes"}], + }, {"kind": "document", "label": "no id at all", "content": "skipped"}, ] queries = [ @@ -1401,12 +1509,22 @@ def gen_high_risk() -> dict: entries = { "3": { - "kind": "ssh", "type": "ssh", "label": "prod-server", "index": 3, - "notes": "deploy", "archived": False, "modified_ts": 1700000000, + "kind": "ssh", + "type": "ssh", + "label": "prod-server", + "index": 3, + "notes": "deploy", + "archived": False, + "modified_ts": 1700000000, }, "5": { - "kind": "pgp", "type": "pgp", "label": "signing", "index": 5, - "notes": "", "archived": False, "modified_ts": 1700000100, + "kind": "pgp", + "type": "pgp", + "label": "signing", + "index": 5, + "notes": "", + "archived": False, + "modified_ts": 1700000100, }, } @@ -1464,16 +1582,31 @@ def gen_index0() -> dict: managed_dir = "/home/user/.seedpass/C557EEC878DFD852/accounts/AABBCCDDEEFF0011" entries = { - "0": {"kind": "password", "label": "bank.example", "modified_ts": 1700000100, - "tags": ["money", "daily"], - "links": [{"target_id": "2", "relation": "depends_on", "note": "recovery"}]}, - "1": {"kind": "totp", "label": "email-2fa", "modified_ts": 1700000200, - "archived": True}, - "2": {"kind": "ssh", "label": "prod \u2014 \u00fcn\u00efcod\u00e9 \u2713", - "modified_ts": 1700000300, "tags": ["ops", "\u00fcn\u00ef"]}, + "0": { + "kind": "password", + "label": "bank.example", + "modified_ts": 1700000100, + "tags": ["money", "daily"], + "links": [{"target_id": "2", "relation": "depends_on", "note": "recovery"}], + }, + "1": { + "kind": "totp", + "label": "email-2fa", + "modified_ts": 1700000200, + "archived": True, + }, + "2": { + "kind": "ssh", + "label": "prod \u2014 \u00fcn\u00efcod\u00e9 \u2713", + "modified_ts": 1700000300, + "tags": ["ops", "\u00fcn\u00ef"], + }, "10": {"kind": "document", "label": "notes", "modified_ts": 1700000400}, - "not-a-number": {"kind": "password", "label": "weird id", - "modified_ts": 1700000500}, + "not-a-number": { + "kind": "password", + "label": "weird id", + "modified_ts": 1700000500, + }, } payload = {"schema_version": 4, "entries": entries} diff --git a/src/seedpass/core/api.py b/src/seedpass/core/api.py index 8462919f..bcad5b19 100644 --- a/src/seedpass/core/api.py +++ b/src/seedpass/core/api.py @@ -1625,10 +1625,7 @@ class SearchService: continue summary[direction][relation] = summary[direction].get(relation, 0) + 1 summary["combined"][relation] = summary["combined"].get(relation, 0) + 1 - return { - key: dict(sorted(value.items())) - for key, value in summary.items() - } + return {key: dict(sorted(value.items())) for key, value in summary.items()} def multi_hop_neighbors( self, @@ -1670,9 +1667,7 @@ class SearchService: # BFS: visited maps neighbor_id -> best result dict visited: dict[int, dict[str, Any]] = {} # frontier: (current_id, current_hop, path_so_far) - frontier: list[tuple[int, int, list[int]]] = [ - (target_id, 0, [target_id]) - ] + frontier: list[tuple[int, int, list[int]]] = [(target_id, 0, [target_id])] while frontier: next_frontier: list[tuple[int, int, list[int]]] = [] @@ -1686,7 +1681,9 @@ class SearchService: if direction_key in {"outgoing", "both"}: for link in self._normalized_links(current_entry): - link_relation = str(link.get("relation", "")).strip().lower() + link_relation = ( + str(link.get("relation", "")).strip().lower() + ) if relation_filter and link_relation != relation_filter: continue neighbor_id = int(link.get("target_id", 0) or 0) @@ -1729,7 +1726,11 @@ class SearchService: if direction_key in {"incoming", "both"}: for source in entries: source_id = int(source.get("id", 0) or 0) - if source_id <= 0 or source_id == target_id or source_id in path_set: + if ( + source_id <= 0 + or source_id == target_id + or source_id in path_set + ): continue archived = bool(source.get("archived", False)) if not include_archived and archived: @@ -1737,7 +1738,9 @@ class SearchService: for link in self._normalized_links(source): if int(link.get("target_id", 0) or 0) != current_id: continue - link_relation = str(link.get("relation", "")).strip().lower() + link_relation = ( + str(link.get("relation", "")).strip().lower() + ) if relation_filter and link_relation != relation_filter: continue source_kind = self._entry_kind(source) diff --git a/src/seedpass/core/index0.py b/src/seedpass/core/index0.py index 96866044..e409023d 100644 --- a/src/seedpass/core/index0.py +++ b/src/seedpass/core/index0.py @@ -487,7 +487,8 @@ def _build_children_view( items = [ _normalize_entry_summary(entry_id, entry) for entry_id, entry in sorted( - _normalize_mapping(entries).items(), key=lambda item: _entry_sort_key(item[0]) + _normalize_mapping(entries).items(), + key=lambda item: _entry_sort_key(item[0]), ) if isinstance(entry, dict) ] diff --git a/src/seedpass/core/manager.py b/src/seedpass/core/manager.py index a3e4e340..5f3489ee 100644 --- a/src/seedpass/core/manager.py +++ b/src/seedpass/core/manager.py @@ -3185,7 +3185,8 @@ class PasswordManager: # call shape means every existing caller and test double that defines # generate_password(length, index) keeps working untouched. kwargs = ( - {} if gen_version == LEGACY_PASSWORD_GEN_VERSION + {} + if gen_version == LEGACY_PASSWORD_GEN_VERSION else {"gen_version": gen_version} ) diff --git a/src/seedpass/tui_v3/app.py b/src/seedpass/tui_v3/app.py index 9d2b1c75..65c5adcc 100644 --- a/src/seedpass/tui_v3/app.py +++ b/src/seedpass/tui_v3/app.py @@ -250,9 +250,7 @@ class CommandProcessor: self.app.action_setting_kdf_mode(args[0]) elif cmd == "setting-kdf-iterations": if not args: - self.app.notify( - "Usage: setting-kdf-iterations ", severity="warning" - ) + self.app.notify("Usage: setting-kdf-iterations ", severity="warning") return self.app.action_setting_kdf_iterations(args[0]) elif cmd == "archive-filter": @@ -2258,9 +2256,7 @@ class SeedPassTuiV3(App[None]): """Switch display density between compact and comfortable.""" normalized = mode.lower() if normalized not in {"compact", "comfortable"}: - self.notify( - "Usage: density ", severity="warning" - ) + self.notify("Usage: density ", severity="warning") return self.density_mode = normalized self.notify(f"Density: {normalized}") diff --git a/src/seedpass/tui_v3/screens/add.py b/src/seedpass/tui_v3/screens/add.py index 59074dad..fe2efc0d 100644 --- a/src/seedpass/tui_v3/screens/add.py +++ b/src/seedpass/tui_v3/screens/add.py @@ -215,7 +215,9 @@ class AddEntryScreen(Screen): ) return - self.app.notify(f"'{label}' ({kind}) saved — press v to reveal or c to copy.") + 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: @@ -322,7 +324,9 @@ class SeedPlusScreen(Screen): else: service.add_managed_account(label=label, index=index, tags=tags) - self.app.notify(f"'{label}' ({kind}) derived at index {index} — reproducible from your parent seed.") + 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: diff --git a/src/seedpass/tui_v3/screens/atlas.py b/src/seedpass/tui_v3/screens/atlas.py index caaaebe7..b9b1a9a6 100644 --- a/src/seedpass/tui_v3/screens/atlas.py +++ b/src/seedpass/tui_v3/screens/atlas.py @@ -120,6 +120,7 @@ class AtlasWayfinderScreen(Screen): return try: from ..widgets.inspector import LinkedItemsPanel # noqa: PLC0415 + panel = self.app.query_one(LinkedItemsPanel) panel.atlas_source_scope = scope_path except Exception: diff --git a/src/seedpass/tui_v3/screens/maintenance.py b/src/seedpass/tui_v3/screens/maintenance.py index 6deb09b6..75f2ec32 100644 --- a/src/seedpass/tui_v3/screens/maintenance.py +++ b/src/seedpass/tui_v3/screens/maintenance.py @@ -1,6 +1,5 @@ from __future__ import annotations - MAINTENANCE_CSS = """ .maintenance-title { background: #000000; diff --git a/src/seedpass/tui_v3/screens/profile.py b/src/seedpass/tui_v3/screens/profile.py index 9b0e1fa2..72948b7e 100644 --- a/src/seedpass/tui_v3/screens/profile.py +++ b/src/seedpass/tui_v3/screens/profile.py @@ -21,9 +21,7 @@ class ProfileManagementScreen(Screen): Binding("escape", "app.pop_screen", "Back", show=True), ] - CSS = ( - MAINTENANCE_CSS - + """ + CSS = MAINTENANCE_CSS + """ ProfileManagementScreen { background: #999999; } @@ -40,7 +38,6 @@ class ProfileManagementScreen(Screen): min-width: 14; } """ - ) def compose(self) -> ComposeResult: yield Static("SeedPass ◈ Profile Management", classes="maintenance-title") @@ -52,17 +49,29 @@ class ProfileManagementScreen(Screen): ) yield Static("", id="profile-list", classes="maintenance-intro-dark") yield Label("Select Profile", classes="maintenance-label") - yield Input(placeholder="Profile number", id="profile-choice", classes="maintenance-input") + yield Input( + placeholder="Profile number", + id="profile-choice", + classes="maintenance-input", + ) yield Label("Password (required for switch)", classes="maintenance-label") - yield Input(password=True, id="profile-password", classes="maintenance-input") + yield Input( + password=True, id="profile-password", classes="maintenance-input" + ) yield Static( - format_status("ready", "Choose a profile number, then use Switch or Remove."), + format_status( + "ready", "Choose a profile number, then use Switch or Remove." + ), id="profile-status", classes="maintenance-status-dark", ) with Horizontal(id="profile-actions", classes="maintenance-actions"): - yield Button("Switch", id="profile-switch", classes="maintenance-primary") - yield Button("Remove", id="profile-remove", classes="maintenance-danger") + yield Button( + "Switch", id="profile-switch", classes="maintenance-primary" + ) + yield Button( + "Remove", id="profile-remove", classes="maintenance-danger" + ) yield Button("Back", id="profile-back", classes="maintenance-secondary") yield Static( "ESC: Back | Ctrl+R: Refresh | Select a profile number to switch or remove", @@ -113,7 +122,9 @@ class ProfileManagementScreen(Screen): if profiles: self.query_one("#profile-choice", Input).value = "1" self._set_status( - format_status("ready", "Choose a profile number, then use Switch or Remove.") + format_status( + "ready", "Choose a profile number, then use Switch or Remove." + ) ) def _selected_profile(self) -> dict[str, str] | None: @@ -124,7 +135,9 @@ class ProfileManagementScreen(Screen): return None idx = int(choice) if idx < 1 or idx > len(profiles): - self._set_status(format_status("warning", "Profile number is out of range.")) + self._set_status( + format_status("warning", "Profile number is out of range.") + ) return None return profiles[idx - 1] @@ -160,7 +173,8 @@ class ProfileManagementScreen(Screen): self._pending_remove_fingerprint = fingerprint self._set_status( format_status( - "warning", f"Confirm: press Remove again to permanently delete profile '{label}'." + "warning", + f"Confirm: press Remove again to permanently delete profile '{label}'.", ) ) return diff --git a/src/seedpass/tui_v3/screens/pubkey.py b/src/seedpass/tui_v3/screens/pubkey.py index c3ac3886..6db15a5e 100644 --- a/src/seedpass/tui_v3/screens/pubkey.py +++ b/src/seedpass/tui_v3/screens/pubkey.py @@ -17,9 +17,7 @@ class NostrPubkeyScreen(Screen): Binding("c", "copy_pubkey", "Copy npub", show=True), ] - CSS = ( - MAINTENANCE_CSS - + """ + CSS = MAINTENANCE_CSS + """ NostrPubkeyScreen { background: #999999; } @@ -36,7 +34,6 @@ class NostrPubkeyScreen(Screen): min-width: 16; } """ - ) def compose(self) -> ComposeResult: yield Static("SeedPass ◈ Active Profile npub", classes="maintenance-title") @@ -52,8 +49,12 @@ class NostrPubkeyScreen(Screen): classes="maintenance-status-light", ) with Horizontal(id="pubkey-actions", classes="maintenance-actions"): - yield Button("Copy npub", id="pubkey-copy", classes="maintenance-primary") - yield Static("ESC: Back | C: Copy npub to clipboard", classes="maintenance-footer") + yield Button( + "Copy npub", id="pubkey-copy", classes="maintenance-primary" + ) + yield Static( + "ESC: Back | C: Copy npub to clipboard", classes="maintenance-footer" + ) def on_mount(self) -> None: self._refresh_view() @@ -86,7 +87,9 @@ class NostrPubkeyScreen(Screen): nostr = self.app.services.get("nostr") entry = self.app.services.get("entry") if not nostr or not entry: - self.app.notify("Clipboard or Nostr service unavailable", severity="warning") + self.app.notify( + "Clipboard or Nostr service unavailable", severity="warning" + ) return try: npub = nostr.get_pubkey() diff --git a/src/seedpass/tui_v3/screens/relays.py b/src/seedpass/tui_v3/screens/relays.py index da5beaf9..1f1ce83f 100644 --- a/src/seedpass/tui_v3/screens/relays.py +++ b/src/seedpass/tui_v3/screens/relays.py @@ -24,9 +24,7 @@ class RelaysScreen(Screen): ("s", "sync_now", "Sync Now"), ] - CSS = ( - MAINTENANCE_CSS - + """ + CSS = MAINTENANCE_CSS + """ RelaysScreen { background: #999999; } @@ -53,7 +51,6 @@ class RelaysScreen(Screen): min-width: 16; } """ - ) def compose(self) -> ComposeResult: yield Static("SeedPass ◈ Nostr Relay Management", classes="maintenance-title") @@ -70,7 +67,9 @@ class RelaysScreen(Screen): id="relay-input", classes="maintenance-input", ) - yield Button("Add Relay", id="add-button", classes="maintenance-primary") + yield Button( + "Add Relay", id="add-button", classes="maintenance-primary" + ) yield Static( format_status("ready", "Select a relay or add a new endpoint."), id="relays-status", diff --git a/src/seedpass/tui_v3/screens/security.py b/src/seedpass/tui_v3/screens/security.py index 79d9100b..9201fa47 100644 --- a/src/seedpass/tui_v3/screens/security.py +++ b/src/seedpass/tui_v3/screens/security.py @@ -17,9 +17,7 @@ class ChangePasswordScreen(Screen): Binding("escape", "app.pop_screen", "Cancel", show=True), ] - CSS = ( - MAINTENANCE_CSS - + """ + CSS = MAINTENANCE_CSS + """ ChangePasswordScreen { background: #999999; } @@ -31,30 +29,47 @@ class ChangePasswordScreen(Screen): min-width: 18; } """ - ) def compose(self) -> ComposeResult: yield Static("SeedPass ◈ Change Vault Password", classes="maintenance-title") - with Container(id="change-password-container", classes="maintenance-panel-dark"): + with Container( + id="change-password-container", classes="maintenance-panel-dark" + ): yield Static( "Change the vault password for the active profile. You must provide the current password to authorize re-encryption.", id="change-password-intro", classes="maintenance-intro-dark", ) yield Label("Current Password", classes="maintenance-label") - yield Input(password=True, id="change-password-old", classes="maintenance-input") + yield Input( + password=True, id="change-password-old", classes="maintenance-input" + ) yield Label("New Password", classes="maintenance-label") - yield Input(password=True, id="change-password-new", classes="maintenance-input") + yield Input( + password=True, id="change-password-new", classes="maintenance-input" + ) yield Label("Confirm New Password", classes="maintenance-label") - yield Input(password=True, id="change-password-confirm", classes="maintenance-input") + yield Input( + password=True, id="change-password-confirm", classes="maintenance-input" + ) yield Static( - format_status("ready", "Enter the current and new password to continue."), + format_status( + "ready", "Enter the current and new password to continue." + ), id="change-password-status", classes="maintenance-status-dark", ) - with Horizontal(id="change-password-actions", classes="maintenance-actions"): - yield Button("Cancel", id="change-password-cancel", classes="maintenance-danger") - yield Button("Update Password", id="change-password-submit", classes="maintenance-primary") + with Horizontal( + id="change-password-actions", classes="maintenance-actions" + ): + yield Button( + "Cancel", id="change-password-cancel", classes="maintenance-danger" + ) + yield Button( + "Update Password", + id="change-password-submit", + classes="maintenance-primary", + ) yield Static("ESC: Cancel | Ctrl+S: Submit", classes="maintenance-footer") def on_button_pressed(self, event: Button.Pressed) -> None: @@ -72,10 +87,14 @@ class ChangePasswordScreen(Screen): confirm = self.query_one("#change-password-confirm", Input).value if not old_password or not new_password: - self._set_status(format_status("warning", "Current and new passwords are required.")) + self._set_status( + format_status("warning", "Current and new passwords are required.") + ) return if new_password != confirm: - self._set_status(format_status("warning", "New password confirmation does not match.")) + self._set_status( + format_status("warning", "New password confirmation does not match.") + ) return self.app.action_change_password(old_password, new_password) @@ -88,9 +107,7 @@ class BackupParentSeedScreen(Screen): Binding("escape", "app.pop_screen", "Cancel", show=True), ] - CSS = ( - MAINTENANCE_CSS - + """ + CSS = MAINTENANCE_CSS + """ BackupParentSeedScreen { background: #999999; } @@ -102,7 +119,6 @@ class BackupParentSeedScreen(Screen): min-width: 18; } """ - ) def compose(self) -> ComposeResult: yield Static("SeedPass ◈ Backup Parent Seed", classes="maintenance-title") @@ -113,17 +129,35 @@ class BackupParentSeedScreen(Screen): classes="maintenance-intro-dark", ) yield Label("Destination Path", classes="maintenance-label") - yield Input(placeholder="seed-backup.enc", id="backup-seed-path", classes="maintenance-input") - yield Label("Backup Passphrase (optional — encrypts the exported seed bundle)", classes="maintenance-label") - yield Input(password=True, id="backup-seed-password", classes="maintenance-input") + yield Input( + placeholder="seed-backup.enc", + id="backup-seed-path", + classes="maintenance-input", + ) + yield Label( + "Backup Passphrase (optional — encrypts the exported seed bundle)", + classes="maintenance-label", + ) + yield Input( + password=True, id="backup-seed-password", classes="maintenance-input" + ) yield Static( - format_status("ready", "Enter the backup destination path to export an encrypted seed bundle."), + format_status( + "ready", + "Enter the backup destination path to export an encrypted seed bundle.", + ), id="backup-seed-status", classes="maintenance-status-dark", ) with Horizontal(id="backup-seed-actions", classes="maintenance-actions"): - yield Button("Cancel", id="backup-seed-cancel", classes="maintenance-danger") - yield Button("Export Backup", id="backup-seed-submit", classes="maintenance-primary") + yield Button( + "Cancel", id="backup-seed-cancel", classes="maintenance-danger" + ) + yield Button( + "Export Backup", + id="backup-seed-submit", + classes="maintenance-primary", + ) yield Static("ESC: Cancel | Ctrl+S: Submit", classes="maintenance-footer") def on_button_pressed(self, event: Button.Pressed) -> None: diff --git a/src/seedpass/tui_v3/screens/settings.py b/src/seedpass/tui_v3/screens/settings.py index 4ea55844..a7eaefe0 100644 --- a/src/seedpass/tui_v3/screens/settings.py +++ b/src/seedpass/tui_v3/screens/settings.py @@ -77,7 +77,9 @@ class SettingsScreen(Screen): return val if val is not None else default except Exception: return "[red](error)[/red]" + else: + def get_val(key, default=""): # type: ignore[misc] return "[dim](offline)[/dim]" diff --git a/src/seedpass/tui_v3/widgets/grid.py b/src/seedpass/tui_v3/widgets/grid.py index 1a8ee6df..ac332968 100644 --- a/src/seedpass/tui_v3/widgets/grid.py +++ b/src/seedpass/tui_v3/widgets/grid.py @@ -45,7 +45,9 @@ class GridMetrics(Static): filter_text = f"Filter: [b]{app.filter_kind}[/b]" sort_text = f"Sort: [b]{app.search_sort}[/b]" query_text = ( - f' Query: [b]"{app.search_query}"[/b]' if str(app.search_query).strip() else "" + f' Query: [b]"{app.search_query}"[/b]' + if str(app.search_query).strip() + else "" ) arch_text = " | [reverse] ARCHIVED [/reverse]" if app.show_archived else "" @@ -241,9 +243,13 @@ class EntryDataTable(DataTable): app.notify(f"Search failed: {e}", severity="error") entries = [] # Fallback to direct semantic search if requested and available - elif app.search_mode != "keyword" and "semantic" in app.services and active_query: + elif ( + app.search_mode != "keyword" and "semantic" in app.services and active_query + ): try: - results = app.services["semantic"].search(active_query, mode=app.search_mode) + results = app.services["semantic"].search( + active_query, mode=app.search_mode + ) # results is list of {entry_id, kind, label, score, excerpt} for r in results: eid = r["entry_id"] diff --git a/src/seedpass/tui_v3/widgets/inspector.py b/src/seedpass/tui_v3/widgets/inspector.py index f14cb8d2..2ef9d694 100644 --- a/src/seedpass/tui_v3/widgets/inspector.py +++ b/src/seedpass/tui_v3/widgets/inspector.py @@ -704,7 +704,9 @@ class BoardContainer(Vertical): def _update_hints(self, board_type: str) -> None: try: - self.query_one("#utility-hints-bar", UtilityHintsBar).update_kind(board_type) + self.query_one("#utility-hints-bar", UtilityHintsBar).update_kind( + board_type + ) except Exception: pass @@ -804,7 +806,15 @@ class LinkedItemsPanel(Vertical): """ # Available kind filters — None means "all" - _KIND_CYCLE: list[str | None] = [None, "password", "nostr", "seed", "document", "note", "totp"] + _KIND_CYCLE: list[str | None] = [ + None, + "password", + "nostr", + "seed", + "document", + "note", + "totp", + ] def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) @@ -829,10 +839,14 @@ class LinkedItemsPanel(Vertical): yield PivotBreadcrumbBar(id="pivot-breadcrumb-bar") yield Label("Linked Items", id="linked-items-title") yield Label("", id="linked-atlas-context") - yield Label("No linked items.", id="linked-items-summary", classes="linked-summary") + yield Label( + "No linked items.", id="linked-items-summary", classes="linked-summary" + ) yield Label("", id="linked-filter-bar", classes="linked-filter-bar") with Vertical(id="linked-items-list"): - yield Label("Select an entry to inspect relationships.", classes="linked-empty") + yield Label( + "Select an entry to inspect relationships.", classes="linked-empty" + ) yield Label("", id="linked-nav-hint", classes="linked-nav-hint") def _active_kind_filter(self) -> str | None: @@ -848,7 +862,10 @@ class LinkedItemsPanel(Vertical): if kind is None: self._current_filter_idx = idx return - if any(str(item.get("kind", "")).strip().lower() == kind for item in neighbors_all): + if any( + str(item.get("kind", "")).strip().lower() == kind + for item in neighbors_all + ): self._current_filter_idx = idx return # No other kind found — stay at all @@ -883,7 +900,8 @@ class LinkedItemsPanel(Vertical): return visible = [ - item for item in neighbors + item + for item in neighbors if kind_filter is None or str(item.get("kind", "")).strip().lower() == kind_filter ] @@ -920,7 +938,10 @@ class LinkedItemsPanel(Vertical): ) ) for item in groups[(direction, relation)]: - label = str(item.get("label", "")).strip() or f"Entry #{item.get('entry_id', '?')}" + label = ( + str(item.get("label", "")).strip() + or f"Entry #{item.get('entry_id', '?')}" + ) kind = str(item.get("kind", "")).strip() or "entry" archived_tag = " [archived]" if item.get("archived") else "" tags = item.get("tags", []) @@ -948,7 +969,9 @@ class LinkedItemsPanel(Vertical): """Fetch label for an entry, falling back to its ID string.""" try: entry = self.app.services["entry"].retrieve_entry(entry_id) - return str(entry.get("label") or f"#{entry_id}") if entry else f"#{entry_id}" + return ( + str(entry.get("label") or f"#{entry_id}") if entry else f"#{entry_id}" + ) except Exception: return f"#{entry_id}" @@ -962,7 +985,9 @@ class LinkedItemsPanel(Vertical): if not history or current_entry_id is None: bar.clear() return - crumbs = [{"entry_id": eid, "label": self._get_entry_label(eid)} for eid in history] + crumbs = [ + {"entry_id": eid, "label": self._get_entry_label(eid)} for eid in history + ] current_label = self._get_entry_label(current_entry_id) bar.set_crumbs(crumbs, current_label) @@ -971,7 +996,9 @@ class LinkedItemsPanel(Vertical): try: ctx_label = self.query_one("#linked-atlas-context", Label) if self.atlas_source_scope: - ctx_label.update(f"[dim]◈ Atlas context: {self.atlas_source_scope}[/dim]") + ctx_label.update( + f"[dim]◈ Atlas context: {self.atlas_source_scope}[/dim]" + ) else: ctx_label.update("") except Exception: @@ -1027,14 +1054,20 @@ class LinkedItemsPanel(Vertical): ) return - outgoing = ", ".join( - f"{relation}:{count}" - for relation, count in rel_summary.get("outgoing", {}).items() - ) or "none" - incoming = ", ".join( - f"{relation}:{count}" - for relation, count in rel_summary.get("incoming", {}).items() - ) or "none" + outgoing = ( + ", ".join( + f"{relation}:{count}" + for relation, count in rel_summary.get("outgoing", {}).items() + ) + or "none" + ) + incoming = ( + ", ".join( + f"{relation}:{count}" + for relation, count in rel_summary.get("incoming", {}).items() + ) + or "none" + ) summary_label.update(f"Outgoing {outgoing} | Incoming {incoming}") kind_filter = self._active_kind_filter() @@ -1042,7 +1075,11 @@ class LinkedItemsPanel(Vertical): filter_bar.update(f"[b]Filter:[/b] {kind_filter} (f: cycle kinds)") else: kinds_present = sorted( - {str(item.get("kind", "")).strip().lower() for item in neighbors if item.get("kind")} + { + str(item.get("kind", "")).strip().lower() + for item in neighbors + if item.get("kind") + } ) kinds_str = ", ".join(kinds_present) if kinds_present else "none" filter_bar.update(f"[dim]All kinds: {kinds_str} (f: filter by kind)[/dim]") @@ -1086,7 +1123,9 @@ class LinkedItemsPanel(Vertical): if search is None: return try: - all_neighbors = search.linked_neighbors(entry_id, direction="both", limit=20) + all_neighbors = search.linked_neighbors( + entry_id, direction="both", limit=20 + ) except Exception: return self._cycle_kind_filter(all_neighbors) diff --git a/src/tests/test_archive_from_retrieve.py b/src/tests/test_archive_from_retrieve.py index b4b3a761..e06cbf96 100644 --- a/src/tests/test_archive_from_retrieve.py +++ b/src/tests/test_archive_from_retrieve.py @@ -15,7 +15,9 @@ from seedpass.core.config_manager import ConfigManager class FakePasswordGenerator: - def generate_password(self, length: int, index: int, gen_version: int = 1) -> str: # noqa: D401 + def generate_password( + self, length: int, index: int, gen_version: int = 1 + ) -> str: # noqa: D401 return "pw" diff --git a/src/tests/test_custom_fields_display.py b/src/tests/test_custom_fields_display.py index cb07713b..68ec8464 100644 --- a/src/tests/test_custom_fields_display.py +++ b/src/tests/test_custom_fields_display.py @@ -7,7 +7,9 @@ sys.path.append(str(Path(__file__).resolve().parents[1])) def test_retrieve_entry_shows_custom_fields(monkeypatch, capsys, password_manager): pm = password_manager - pm.password_generator = SimpleNamespace(generate_password=lambda l, i, gen_version=1: "pw") + pm.password_generator = SimpleNamespace( + generate_password=lambda l, i, gen_version=1: "pw" + ) pm.nostr_client = SimpleNamespace() pm.entry_manager.add_entry( diff --git a/src/tests/test_delta_merge.py b/src/tests/test_delta_merge.py index 10f9de7b..1760a4c7 100644 --- a/src/tests/test_delta_merge.py +++ b/src/tests/test_delta_merge.py @@ -520,12 +520,15 @@ def test_merged_deletion_does_not_recycle_entry_index(): incoming = { "schema_version": 4, "entries": { - "2": {"kind": "password", "label": "web-c", "modified_ts": 10**10, "_deleted": True}, + "2": { + "kind": "password", + "label": "web-c", + "modified_ts": 10**10, + "_deleted": True, + }, }, } - merged = merge_index_payloads( - vault.load_index(), incoming, source_tag="remote" - ) + merged = merge_index_payloads(vault.load_index(), incoming, source_tag="remote") assert "2" not in merged["entries"] assert merged["_sync_meta"]["next_index"] == 3 vault.save_index(merged) diff --git a/src/tests/test_entropy_integrity.py b/src/tests/test_entropy_integrity.py index 4f5c3b1d..4f0801ab 100644 --- a/src/tests/test_entropy_integrity.py +++ b/src/tests/test_entropy_integrity.py @@ -112,9 +112,7 @@ V1_VECTORS = [ def _generator(policy: PasswordPolicy) -> PasswordGenerator: seed = Bip39SeedGenerator(TEST_MNEMONIC).Generate() - return PasswordGenerator( - _SeedDeriver(), TEST_MNEMONIC, BIP85(seed), policy=policy - ) + return PasswordGenerator(_SeedDeriver(), TEST_MNEMONIC, BIP85(seed), policy=policy) # -------------------------------------------------------------------------- @@ -422,9 +420,7 @@ def test_entry_with_gen_version_2_derives_v2(): pm = _manager_with(pg) entry = {"length": 16, "gen_version": CURRENT_PASSWORD_GEN_VERSION} got = PasswordManager._generate_password_for_entry(pm, entry, 3) - assert got == pg.generate_password( - 16, 3, gen_version=CURRENT_PASSWORD_GEN_VERSION - ) + assert got == pg.generate_password(16, 3, gen_version=CURRENT_PASSWORD_GEN_VERSION) assert got != pg.generate_password(16, 3, gen_version=1) diff --git a/src/tests/test_manager_add_password.py b/src/tests/test_manager_add_password.py index 4fc950db..509b8d1a 100644 --- a/src/tests/test_manager_add_password.py +++ b/src/tests/test_manager_add_password.py @@ -17,7 +17,9 @@ from constants import DEFAULT_PASSWORD_LENGTH class FakePasswordGenerator: - def generate_password(self, length: int, index: int, gen_version: int = 1) -> str: # noqa: D401 + def generate_password( + self, length: int, index: int, gen_version: int = 1 + ) -> str: # noqa: D401 return f"pw-{index}-{length}" diff --git a/src/tests/test_manager_checksum_backup.py b/src/tests/test_manager_checksum_backup.py index c2061346..7f4ec12a 100644 --- a/src/tests/test_manager_checksum_backup.py +++ b/src/tests/test_manager_checksum_backup.py @@ -35,7 +35,9 @@ def test_handle_verify_checksum_success(monkeypatch, tmp_path, capsys): chk_file.write_text("abc") monkeypatch.setattr("seedpass.core.manager.SCRIPT_CHECKSUM_FILE", chk_file) monkeypatch.setattr( - type(pm), "_script_checksum_status", lambda self: (True, tmp_path / "seedpass.py") + type(pm), + "_script_checksum_status", + lambda self: (True, tmp_path / "seedpass.py"), ) pm.handle_verify_checksum() assert "Checksum verification passed." in capsys.readouterr().out @@ -47,7 +49,9 @@ def test_handle_verify_checksum_failure(monkeypatch, tmp_path, capsys): chk_file.write_text("xyz") monkeypatch.setattr("seedpass.core.manager.SCRIPT_CHECKSUM_FILE", chk_file) monkeypatch.setattr( - type(pm), "_script_checksum_status", lambda self: (False, tmp_path / "seedpass.py") + type(pm), + "_script_checksum_status", + lambda self: (False, tmp_path / "seedpass.py"), ) pm.handle_verify_checksum() assert "Checksum verification failed" in capsys.readouterr().out @@ -61,7 +65,9 @@ def test_handle_verify_checksum_target_mismatch(monkeypatch, tmp_path, capsys): chk_file.write_text("abc") monkeypatch.setattr("seedpass.core.manager.SCRIPT_CHECKSUM_FILE", chk_file) monkeypatch.setattr( - type(pm), "_script_checksum_status", lambda self: (None, tmp_path / "seedpass.py") + type(pm), + "_script_checksum_status", + lambda self: (None, tmp_path / "seedpass.py"), ) pm.handle_verify_checksum() out = capsys.readouterr().out @@ -74,7 +80,9 @@ def test_handle_verify_checksum_missing(monkeypatch, tmp_path, capsys): missing = tmp_path / "absent.txt" monkeypatch.setattr("seedpass.core.manager.SCRIPT_CHECKSUM_FILE", missing) monkeypatch.setattr( - type(pm), "_script_checksum_status", lambda self: (True, tmp_path / "seedpass.py") + type(pm), + "_script_checksum_status", + lambda self: (True, tmp_path / "seedpass.py"), ) pm.handle_verify_checksum() note = pm.notifications.get_nowait() diff --git a/src/tests/test_manager_list_entries.py b/src/tests/test_manager_list_entries.py index 9e63ddbc..660a4ed9 100644 --- a/src/tests/test_manager_list_entries.py +++ b/src/tests/test_manager_list_entries.py @@ -138,7 +138,9 @@ def test_show_entry_details_by_index(monkeypatch): monkeypatch.setattr( "seedpass.core.manager.confirm_action", lambda *a, **k: False ) - pm.password_generator = SimpleNamespace(generate_password=lambda l, i, gen_version=1: "pw123") + pm.password_generator = SimpleNamespace( + generate_password=lambda l, i, gen_version=1: "pw123" + ) monkeypatch.setattr(pm, "notify", lambda *a, **k: None) pm.show_entry_details_by_index(index) @@ -177,7 +179,9 @@ def _detail_common(monkeypatch, pm): monkeypatch.setattr("seedpass.core.manager.timed_input", lambda *a, **k: "b") monkeypatch.setattr("seedpass.core.manager.time.sleep", lambda *a, **k: None) monkeypatch.setattr(pm, "notify", lambda *a, **k: None) - pm.password_generator = SimpleNamespace(generate_password=lambda l, i, gen_version=1: "pw123") + pm.password_generator = SimpleNamespace( + generate_password=lambda l, i, gen_version=1: "pw123" + ) called = [] monkeypatch.setattr(pm, "_entry_actions_menu", lambda *a, **k: called.append(True)) return called @@ -295,7 +299,9 @@ def test_show_entry_details_sensitive(monkeypatch, capsys, entry_type): with TemporaryDirectory() as tmpdir: tmp_path = Path(tmpdir) pm, entry_mgr = _setup_manager(tmp_path) - pm.password_generator = SimpleNamespace(generate_password=lambda l, i, gen_version=1: "pw123") + pm.password_generator = SimpleNamespace( + generate_password=lambda l, i, gen_version=1: "pw123" + ) monkeypatch.setattr( "seedpass.core.manager.confirm_action", lambda *a, **k: True diff --git a/src/tests/test_manager_workflow.py b/src/tests/test_manager_workflow.py index cd94ac25..c74e6906 100644 --- a/src/tests/test_manager_workflow.py +++ b/src/tests/test_manager_workflow.py @@ -13,7 +13,9 @@ from seedpass.core.config_manager import ConfigManager class FakePasswordGenerator: - def generate_password(self, length: int, index: int, gen_version: int = 1) -> str: # noqa: D401 + def generate_password( + self, length: int, index: int, gen_version: int = 1 + ) -> str: # noqa: D401 return f"pw-{index}-{length}" diff --git a/src/tests/test_manifest_ordering.py b/src/tests/test_manifest_ordering.py index c080a7b7..09e93815 100644 --- a/src/tests/test_manifest_ordering.py +++ b/src/tests/test_manifest_ordering.py @@ -59,7 +59,9 @@ class FakeEvent: def test_order_key_prefers_published_ms(): - with_ms = Manifest(ver=1, algo="gzip", chunks=[], published_ms=SAME_SECOND * 1000 + 900) + with_ms = Manifest( + ver=1, algo="gzip", chunks=[], published_ms=SAME_SECOND * 1000 + 900 + ) assert manifest_order_ms(with_ms, SAME_SECOND) == SAME_SECOND * 1000 + 900 @@ -96,8 +98,14 @@ def test_event_id_still_breaks_an_exact_millisecond_tie(): # contents, so no party chooses the winner. a = FakeEvent("1111", SAME_SECOND, SAME_SECOND * 1000 + 500) b = FakeEvent("2222", SAME_SECOND, SAME_SECOND * 1000 + 500) - assert [e.id for e in _sort_manifest_events_newest_first([a, b])] == ["2222", "1111"] - assert [e.id for e in _sort_manifest_events_newest_first([b, a])] == ["2222", "1111"] + assert [e.id for e in _sort_manifest_events_newest_first([a, b])] == [ + "2222", + "1111", + ] + assert [e.id for e in _sort_manifest_events_newest_first([b, a])] == [ + "2222", + "1111", + ] def test_an_unparseable_manifest_orders_by_created_at_without_raising(): diff --git a/src/tests/test_password_notes_display.py b/src/tests/test_password_notes_display.py index e9509ec5..b7d3ef5d 100644 --- a/src/tests/test_password_notes_display.py +++ b/src/tests/test_password_notes_display.py @@ -27,7 +27,9 @@ def test_password_notes_shown(monkeypatch, capsys): pm.vault = vault pm.entry_manager = entry_mgr pm.backup_manager = backup_mgr - pm.password_generator = SimpleNamespace(generate_password=lambda l, i, **_: "pw") + pm.password_generator = SimpleNamespace( + generate_password=lambda l, i, **_: "pw" + ) pm.parent_seed = TEST_SEED pm.nostr_client = SimpleNamespace() pm.fingerprint_dir = tmp_path diff --git a/src/tests/test_semantic_index.py b/src/tests/test_semantic_index.py index f50d6822..a255da75 100644 --- a/src/tests/test_semantic_index.py +++ b/src/tests/test_semantic_index.py @@ -265,7 +265,7 @@ def test_an_index_built_before_the_fix_is_deleted_on_first_touch(tmp_path): def test_an_unreadable_manifest_is_not_treated_as_stale(tmp_path): - """"Cannot tell" must not mean "delete it".""" + """ "Cannot tell" must not mean "delete it".""" from seedpass.core.semantic_index import SemanticIndex index = SemanticIndex(tmp_path) diff --git a/src/tests/test_tui_v3_parity.py b/src/tests/test_tui_v3_parity.py index e1269c6d..f713e050 100644 --- a/src/tests/test_tui_v3_parity.py +++ b/src/tests/test_tui_v3_parity.py @@ -365,6 +365,7 @@ async def test_v3_action_bar_global_row_uses_correct_handlers() -> None: # 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 diff --git a/src/tests/test_tui_v3_smoke.py b/src/tests/test_tui_v3_smoke.py index 64b132db..46bc858b 100644 --- a/src/tests/test_tui_v3_smoke.py +++ b/src/tests/test_tui_v3_smoke.py @@ -986,9 +986,7 @@ async def test_tui3_recover_profile_sets_success_status_before_bootstrap() -> No booted["fingerprint"] = fingerprint # Capture status text at the moment bootstrap fires try: - status_at_boot.append( - str(app.screen.query_one("#recover-status").render()) - ) + status_at_boot.append(str(app.screen.query_one("#recover-status").render())) except Exception: pass @@ -1219,4 +1217,6 @@ async def test_tui3_toolbar_active_class_reflects_app_state() -> None: app.action_set_search_sort("label_asc") await pilot.pause() assert app.screen.query_one("#grid-sort-label_asc", Button).has_class("active") - assert not app.screen.query_one("#grid-sort-relevance", Button).has_class("active") + assert not app.screen.query_one("#grid-sort-relevance", Button).has_class( + "active" + )