mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-11-01 18:32:05 +00:00
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:
1 parent
71db3bc498
commit
3395aadd1c
32 files changed
+686
-249
No files matched your search
@@ -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
@@ -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}
|
||||
|
||||
|
||||
@@ -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} ---")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
]
|
||||
|
||||
@@ -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}
|
||||
)
|
||||
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]"
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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}"
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
|
||||
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
Reference in new issue
Block a user