Fix three CI failures: a stale black, my scratchpad path, an impossible check

First push of this branch in a while, and CI found three things.

BLACK VERSION DRIFT. The repo pins black ^24.3 (24.10.0 locked) and the local
environment has 26.1.0, whose stable style differs. An earlier commit titled
"satisfy the black gate" reformatted five tui_v3 screens with 26.1.0, which
UNformatted them as far as CI was concerned — the fix made the gate worse.
Everything is now formatted with 24.10.0, the pinned version, reproducing
CI's six files exactly before and zero after. scripts/mutation_test.py is in
there because I wrote it this session and never ran black on it at all.

MY SCRATCHPAD PATH IN requirements.lock. pip-compile records the command that
generated a lockfile in its header, and when I regenerated it during the
dependency-vulnerability fix I compiled to a temp path and copied the result
in. That baked an absolute path from my machine into a committed file, so
CI's regenerate-and-diff could never match. Regenerated with CI's exact
command on Python 3.11; only the two header lines change, so the dependency
set was never wrong.

AN UNSATISFIABLE LOCKFILE CHECK. Separately, and not caused by me: the step
regenerated the lockfile and diffed it on every matrix entry, including
Windows on 3.10 while Linux and macOS use 3.11. pip-compile resolves
version-conditional dependencies per interpreter, so those two produce
genuinely different files — 135 lines apart here, argon2-cffi-bindings among
them. One committed lockfile cannot equal both outputs, so whichever version
it was built on, the other job failed. Verification now runs once on
Linux/3.11, the canonical generator; every platform still installs from the
lockfile with --require-hashes, which is the part that had to stay everywhere.

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-20 09:14:19 -04:00
1 parent bc6ee2288e
commit 4204a23cab
8 files changed
+67 -27

No files matched your search

+14 -1
View File
@@ -76,12 +76,25 @@ jobs:
key: ${{ runner.os }}-pip-${{ hashFiles('requirements.lock') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Verify lockfile and install dependencies
# Regenerating the lockfile and diffing it proves the committed file
# really came from src/requirements.txt. It can only be done on ONE
# interpreter: pip-compile resolves version-conditional dependencies
# per Python version, so 3.10 and 3.11 produce genuinely different
# files (argon2-cffi-bindings among others), and a single committed
# lockfile cannot equal both. Run as written across this matrix the
# check was unsatisfiable -- whichever version the lockfile was built
# on, the other job failed. Linux/3.11 is the canonical generator, so
# the verification lives there.
- name: Verify the lockfile matches src/requirements.txt
if: runner.os == 'Linux'
run: |
python -m pip install --upgrade pip
pip install pip-tools
pip-compile --generate-hashes --output-file=requirements.lock src/requirements.txt
git diff --exit-code requirements.lock
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install --require-hashes -r requirements.lock
- name: Run dependency scan
run: scripts/dependency_scan.sh --ignore-vuln GHSA-wj6h-64fc-37mp --ignore-vuln CVE-2025-62727
+2 -2
View File
@@ -1,8 +1,8 @@
#
# This file is autogenerated by pip-compile with Python 3.12
# This file is autogenerated by pip-compile with Python 3.11
# by the following command:
#
# pip-compile --generate-hashes --output-file=/tmp/claude-1000/-home-user/4efd790e-427f-430a-b379-ef8391e6744f/scratchpad/req.lock src/requirements.txt
# pip-compile --generate-hashes --output-file=requirements.lock src/requirements.txt
#
aiohappyeyeballs==2.7.1 \
--hash=sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d \
+27 -18
View File
@@ -77,6 +77,7 @@ def acquire_lock() -> None:
def release_lock() -> None:
LOCK.unlink(missing_ok=True)
# Each target names the source file and the tests that should defend it.
# Running only the relevant suites keeps a full pass tractable; a mutant that
# survives its own suite but would be caught by another is still a finding,
@@ -123,8 +124,7 @@ SECURITY_HINTS = re.compile(
# `kinds` list is half of what it may reach -- and leaving it out hid the
# shape check that stops a string `kinds` becoming a substring match.
# `mnemonic`/`seed` guard the material everything else protects.
r"kind|mnemonic|seed"
r")",
r"kind|mnemonic|seed" r")",
re.IGNORECASE,
)
@@ -192,9 +192,7 @@ def generate(source: str) -> list[Mutant]:
mutated = line.replace(old, new, 1)
if mutated == line:
continue
mutants.append(
Mutant(i, line, mutated, f"{old.strip()} -> {new.strip()}")
)
mutants.append(Mutant(i, line, mutated, f"{old.strip()} -> {new.strip()}"))
# Force a guard to always allow. This is the single most informative
# mutation for security code: if no test notices a check being removed
# entirely, that check is unverified.
@@ -221,7 +219,10 @@ def generate(source: str) -> list[Mutant]:
if (
inline
and not line.rstrip().endswith("{")
and (SECURITY_HINTS.search(inline.group(2)) or REFUSAL.search(inline.group(3)))
and (
SECURITY_HINTS.search(inline.group(2))
or REFUSAL.search(inline.group(3))
)
):
mutants.append(
Mutant(
@@ -247,9 +248,7 @@ def run(cmd: list[str], cwd: Path, timeout: int) -> tuple[int, str]:
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--target", default="all", choices=[*sorted(TARGETS), "all"]
)
parser.add_argument("--target", default="all", choices=[*sorted(TARGETS), "all"])
parser.add_argument(
"--limit", type=int, default=0, help="cap mutants per target (0 = all)"
)
@@ -335,12 +334,12 @@ def main() -> int:
lines[mutant.line_no - 1] = mutant.mutated + ending
path.write_text("".join(lines))
code, _ = run(
["npx", "tsc", "--noEmit"], CLI, timeout=args.timeout
)
code, _ = run(["npx", "tsc", "--noEmit"], CLI, timeout=args.timeout)
if code != 0:
overall_counts["invalid"] += 1
print(f" [{idx}/{len(all_mutants)}] line {mutant.line_no}: invalid (does not typecheck)")
print(
f" [{idx}/{len(all_mutants)}] line {mutant.line_no}: invalid (does not typecheck)"
)
continue
code, _ = run(
@@ -348,11 +347,15 @@ def main() -> int:
)
if code != 0:
overall_counts["killed"] += 1
print(f" [{idx}/{len(all_mutants)}] line {mutant.line_no}: killed ({mutant.description})")
print(
f" [{idx}/{len(all_mutants)}] line {mutant.line_no}: killed ({mutant.description})"
)
else:
overall_counts["survived"] += 1
overall_survivors.append((f"{rel}:{mutant.line_no}", mutant))
print(f" [{idx}/{len(all_mutants)}] line {mutant.line_no}: SURVIVED ({mutant.description})")
print(
f" [{idx}/{len(all_mutants)}] line {mutant.line_no}: SURVIVED ({mutant.description})"
)
finally:
restore()
# Prove the restore worked rather than assuming it: a mutant left
@@ -360,7 +363,9 @@ def main() -> int:
# tree waiting to be committed.
if path.read_text() != original:
release_lock()
raise SystemExit(f"FAILED TO RESTORE {rel} -- fix this before committing")
raise SystemExit(
f"FAILED TO RESTORE {rel} -- fix this before committing"
)
release_lock()
@@ -372,7 +377,9 @@ def main() -> int:
f"{overall_counts['invalid']} invalid (of {total})"
)
if viable:
print(f"mutation score: {overall_counts['killed'] / viable:.0%} of viable mutants caught")
print(
f"mutation score: {overall_counts['killed'] / viable:.0%} of viable mutants caught"
)
if unmeasured:
print(
@@ -383,7 +390,9 @@ def main() -> int:
print(f" {where}")
if overall_survivors:
print(f"\nSURVIVORS — each is either a gap or an equivalent mutant, and needs judging:\n")
print(
f"\nSURVIVORS — each is either a gap or an equivalent mutant, and needs judging:\n"
)
for where, mutant in overall_survivors:
print(f" {where} [{mutant.description}]")
print(f" was: {mutant.original.strip()[:110]}")
+4 -1
View File
@@ -53,7 +53,9 @@ def render_wayfinder(payload: dict) -> str:
class AtlasWayfinderScreen(Screen):
CSS = MAINTENANCE_CSS + """
CSS = (
MAINTENANCE_CSS
+ """
#atlas-wayfinder-shell { background: #101010; }
#atlas-wayfinder-actions {
height: auto;
@@ -71,6 +73,7 @@ class AtlasWayfinderScreen(Screen):
margin: 1 2;
}
"""
)
def __init__(self, payload: dict) -> None:
super().__init__()
+4 -1
View File
@@ -21,7 +21,9 @@ class ProfileManagementScreen(Screen):
Binding("escape", "app.pop_screen", "Back", show=True),
]
CSS = MAINTENANCE_CSS + """
CSS = (
MAINTENANCE_CSS
+ """
ProfileManagementScreen {
background: #999999;
}
@@ -38,6 +40,7 @@ class ProfileManagementScreen(Screen):
min-width: 14;
}
"""
)
def compose(self) -> ComposeResult:
yield Static("SeedPass ◈ Profile Management", classes="maintenance-title")
+4 -1
View File
@@ -17,7 +17,9 @@ class NostrPubkeyScreen(Screen):
Binding("c", "copy_pubkey", "Copy npub", show=True),
]
CSS = MAINTENANCE_CSS + """
CSS = (
MAINTENANCE_CSS
+ """
NostrPubkeyScreen {
background: #999999;
}
@@ -34,6 +36,7 @@ class NostrPubkeyScreen(Screen):
min-width: 16;
}
"""
)
def compose(self) -> ComposeResult:
yield Static("SeedPass ◈ Active Profile npub", classes="maintenance-title")
+4 -1
View File
@@ -24,7 +24,9 @@ class RelaysScreen(Screen):
("s", "sync_now", "Sync Now"),
]
CSS = MAINTENANCE_CSS + """
CSS = (
MAINTENANCE_CSS
+ """
RelaysScreen {
background: #999999;
}
@@ -51,6 +53,7 @@ class RelaysScreen(Screen):
min-width: 16;
}
"""
)
def compose(self) -> ComposeResult:
yield Static("SeedPass ◈ Nostr Relay Management", classes="maintenance-title")
+8 -2
View File
@@ -17,7 +17,9 @@ class ChangePasswordScreen(Screen):
Binding("escape", "app.pop_screen", "Cancel", show=True),
]
CSS = MAINTENANCE_CSS + """
CSS = (
MAINTENANCE_CSS
+ """
ChangePasswordScreen {
background: #999999;
}
@@ -29,6 +31,7 @@ class ChangePasswordScreen(Screen):
min-width: 18;
}
"""
)
def compose(self) -> ComposeResult:
yield Static("SeedPass ◈ Change Vault Password", classes="maintenance-title")
@@ -107,7 +110,9 @@ class BackupParentSeedScreen(Screen):
Binding("escape", "app.pop_screen", "Cancel", show=True),
]
CSS = MAINTENANCE_CSS + """
CSS = (
MAINTENANCE_CSS
+ """
BackupParentSeedScreen {
background: #999999;
}
@@ -119,6 +124,7 @@ class BackupParentSeedScreen(Screen):
min-width: 18;
}
"""
)
def compose(self) -> ComposeResult:
yield Static("SeedPass ◈ Backup Parent Seed", classes="maintenance-title")