style: satisfy the black gate that was failing the Tests workflow

The Tests workflow was red on every OS and Python version for one reason:
`black --check` found 28 unformatted files. Part predates this branch (the
tui_v3 screens, core/api.py, core/index0.py); part is mine from this session.

Formatting only — verified rather than assumed, because a formatter touching
363 files is exactly where an unnoticed behaviour change would hide:

  - 1223 Python tests pass, unchanged
  - regenerating the fixtures after reformatting the generator produces
    byte-identical output, so no hashed value moved
  - differential fuzz: no divergences

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
This commit is contained in:
thePR0M3TH3ANandClaude Opus 5 committed 2026-08-19 12:22:19 -04:00
1 parent 71db3bc498
commit 3395aadd1c
32 files changed
+686 -249

No files matched your search

+1 -1
View File
@@ -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"},
+189 -54
View File
@@ -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", "[email protected]",
app_dir,
"entry",
"add",
"pgp",
"ts-pgp",
"--user-id",
"[email protected]",
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}
+50 -17
View File
@@ -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} ---")
+2
View File
@@ -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
+188 -55
View File
@@ -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, "[email protected]"), (3, ""), (7, "Test User <[email protected]>")):
for index, user_id in (
(0, "[email protected]"),
(3, ""),
(7, "Test User <[email protected]>"),
):
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}
+13 -10
View File
@@ -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)
+2 -1
View File
@@ -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)
]
+2 -1
View File
@@ -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}
)
+2 -6
View File
@@ -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 <n>", severity="warning"
)
self.app.notify("Usage: setting-kdf-iterations <n>", 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 <compact|comfortable>", severity="warning"
)
self.notify("Usage: density <compact|comfortable>", severity="warning")
return
self.density_mode = normalized
self.notify(f"Density: {normalized}")
+6 -2
View File
@@ -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:
+1
View File
@@ -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:
@@ -1,6 +1,5 @@
from __future__ import annotations
MAINTENANCE_CSS = """
.maintenance-title {
background: #000000;
+26 -12
View File
@@ -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
+10 -7
View File
@@ -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()
+4 -5
View File
@@ -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",
+58 -24
View File
@@ -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:
+2
View File
@@ -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]"
+9 -3
View File
@@ -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"]
+59 -20
View File
@@ -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)
+3 -1
View File
@@ -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"
+3 -1
View File
@@ -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(
+7 -4
View File
@@ -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)
+2 -6
View File
@@ -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)
+3 -1
View File
@@ -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}"
+12 -4
View File
@@ -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()
+9 -3
View File
@@ -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
+3 -1
View File
@@ -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}"
+11 -3
View File
@@ -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():
+3 -1
View File
@@ -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
+1 -1
View File
@@ -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)
+1
View File
@@ -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
+4 -4
View File
@@ -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"
)