Skip to content
This repository was archived by the owner on Aug 22, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion generate_it/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
SPECIAL_CHARACTERS = "!@#$%^&*()-_=+[]{};:,.?/"

# Username-related character sets.
USERNAME_ALPHANUMERIC = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
USERNAME_ALPHANUMERIC = "abcdefghijklmnopqrstuvwxyz0123456789"
USERNAME_SEPARATORS = frozenset(["_", "-"])

# Username styles
Expand Down
6 changes: 3 additions & 3 deletions generate_it/tui.py
Original file line number Diff line number Diff line change
Expand Up @@ -2063,9 +2063,9 @@ def _gen_pwd():
return 0


# Imported last to avoid a circular import: tui_render aliases a few
# state-label helpers (e.g. _selected_category_count) that are defined
# above in this module.
# Imported last to avoid a circular import: tui_render now redefines a few
# state-label helpers (e.g. _selected_category_count) locally rather
# than importing them from this module.
from . import tui_render as R
from . import tui_flow

Expand Down
61 changes: 29 additions & 32 deletions generate_it/tui_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -772,16 +772,6 @@ def _render_info_box(
inner_w = max(0, w - 4)
row = y + 1

bits = _estimate_entropy_bits(state, wordlist_size)
label, kind = _strength_label(bits)

if kind == "bad":
kind_attr = theme.bad
elif kind == "warn":
kind_attr = theme.warn
else:
kind_attr = theme.ok

mode_str = (
"characters"
if state.mode == "chars"
Expand Down Expand Up @@ -838,26 +828,33 @@ def _render_info_box(
)
row += 1

# Strength bar
_addstr_safe(stdscr, row, x + 2, f"Entropy: ~{bits:0.1f} bits"[:inner_w], theme.dim)
row += 1
if state.mode != "username":
bits = _estimate_entropy_bits(state, wordlist_size)
label, kind = _strength_label(bits)

prefix = "Strength: ["
suffix = f"] {label}"
bar_w = max(0, inner_w - len(prefix) - len(suffix))
if state.mode == "chars":
max_bits = float(generator.MAX_PASSWORD_CHARS) * math.log2(
len(generator.LETTERS) + len(generator.NUMBERS) + len(generator.SPECIAL_CHARACTERS)
)
elif state.mode == "words":
max_bits = (
float(generator.MAX_PASSPHRASE_WORDS) * math.log2(max(2, wordlist_size))
+ 3.0 * math.log2(10.0)
+ math.log2(len(generator.PASSPHRASE_SPECIALS))
)
else:
max_bits = float(generator.MAX_PASSWORD_CHARS) * math.log2(
len(generator.USERNAME_ALPHANUMERIC)
)
bar = _bar(min(bits, max_bits), max_bits, bar_w)
_addstr_safe(stdscr, row, x + 2, f"{prefix}{bar}{suffix}"[:inner_w], kind_attr)
if kind == "bad":
kind_attr = theme.bad
elif kind == "warn":
kind_attr = theme.warn
else:
kind_attr = theme.ok

# Strength bar
_addstr_safe(stdscr, row, x + 2, f"Entropy: ~{bits:0.1f} bits"[:inner_w], theme.dim)
row += 1

prefix = "Strength: ["
suffix = f"] {label}"
bar_w = max(0, inner_w - len(prefix) - len(suffix))
if state.mode == "chars":
max_bits = float(generator.MAX_PASSWORD_CHARS) * math.log2(
len(generator.LETTERS) + len(generator.NUMBERS) + len(generator.SPECIAL_CHARACTERS)
)
elif state.mode == "words":
max_bits = (
float(generator.MAX_PASSPHRASE_WORDS) * math.log2(max(2, wordlist_size))
+ 3.0 * math.log2(10.0)
+ math.log2(len(generator.PASSPHRASE_SPECIALS))
)
bar = _bar(min(bits, max_bits), max_bits, bar_w)
_addstr_safe(stdscr, row, x + 2, f"{prefix}{bar}{suffix}"[:inner_w], kind_attr)
117 changes: 117 additions & 0 deletions tests/test_constants_sync.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
"""Test that every constant duplicated across modules matches its counterpart.

This test currently guards a single duplication surface:

**``constants.py`` ↔ ``generator.py``** — generator-level constants
(length bounds, character sets, wordlists, entropy minimums). This is the
original drift surface; ``USERNAME_ALPHANUMERIC`` was caught diverging here
(``review-REJECTED-consolidated-remediation-20260816-2305.md`` §3).

If a test here fails, the two modules are out of sync. Fix the source of
truth (first decide which one it is, then update the other) and add the
correction to this test so future drift is caught.

--------------------------------------------------------------------
Known architectural debt — not yet guarded by this test suite
--------------------------------------------------------------------

**Policy-tuple duplication** — ``constants.py`` ↔ ``tui.py``
``CLIPBOARD_AUTO_CLEAR_OPTIONS`` and ``AUTO_LOCK_OPTIONS`` are defined
independently in both modules (``tui.py`` at lines 52, 62 is the runtime
authority; ``constants.py`` at lines 10, 20 is imported by
``tui_render.py``). These are verified byte-identical at the time of
PR #3 remediation but are *not* guarded by a sync test — they could
silently diverge.

**Constants single-source-of-truth migration**
Making ``generator.py`` and ``tui.py`` import from ``constants.py``
(rather than defining independent copies) is the intended resolution for
*all* duplication surfaces above. The sync test is a pragmatic interim
guard; the full migration is deferred for scope-risk reasons. When the
migration lands, this file should be updated to verify the *remaining*
guards rather than the duplication surfaces.
"""

from __future__ import annotations

import string

from generate_it import constants
from generate_it import generator


def _as_str(value: object) -> str:
"""Normalise a constant to a string for comparison.

Both modules may express string constants as plain ``str``, via
``string.ascii_letters``, or as ``frozenset``. Normalise everything
to a plain ``str`` for unambiguous comparison.
"""
if isinstance(value, frozenset):
return str(sorted(value))
return str(value)


class TestConstantsSync:
"""Verify every shared constant matches between ``constants`` and ``generator``."""

# --- Integer/bool constants -----------------------------------------

def test_min_password_chars(self) -> None:
assert constants.MIN_PASSWORD_CHARS == generator.MIN_PASSWORD_CHARS

def test_max_password_chars(self) -> None:
assert constants.MAX_PASSWORD_CHARS == generator.MAX_PASSWORD_CHARS

def test_min_passphrase_words(self) -> None:
assert constants.MIN_PASSPHRASE_WORDS == generator.MIN_PASSPHRASE_WORDS

def test_max_passphrase_words(self) -> None:
assert constants.MAX_PASSPHRASE_WORDS == generator.MAX_PASSPHRASE_WORDS

def test_min_username_length(self) -> None:
assert constants.MIN_USERNAME_LENGTH == generator.MIN_USERNAME_LENGTH

def test_max_username_length(self) -> None:
assert constants.MAX_USERNAME_LENGTH == generator.MAX_USERNAME_LENGTH

def test_min_username_words(self) -> None:
assert constants.MIN_USERNAME_WORDS == generator.MIN_USERNAME_WORDS

def test_max_username_words(self) -> None:
assert constants.MAX_USERNAME_WORDS == generator.MAX_USERNAME_WORDS

def test_min_passphrase_entropy_bits(self) -> None:
assert constants._MIN_PASSPHRASE_ENTROPY_BITS == generator._MIN_PASSPHRASE_ENTROPY_BITS

# --- Character-set string constants ----------------------------------

def test_letters(self) -> None:
assert _as_str(constants.LETTERS) == _as_str(generator.LETTERS)

def test_numbers(self) -> None:
assert _as_str(constants.NUMBERS) == _as_str(generator.NUMBERS)

def test_special_characters(self) -> None:
assert _as_str(constants.SPECIAL_CHARACTERS) == _as_str(generator.SPECIAL_CHARACTERS)

def test_username_alphanumeric(self) -> None:
"""This was the constant that diverged — PR #3 review caught it."""
assert (
_as_str(constants.USERNAME_ALPHANUMERIC)
== _as_str(generator.USERNAME_ALPHANUMERIC)
)
# The canonical value — both must be lowercase-only
expected = string.ascii_lowercase + string.digits
assert _as_str(constants.USERNAME_ALPHANUMERIC) == expected

def test_username_separators(self) -> None:
assert _as_str(constants.USERNAME_SEPARATORS) == _as_str(generator.USERNAME_SEPARATORS)

def test_passphrase_specials(self) -> None:
assert _as_str(constants.PASSPHRASE_SPECIALS) == _as_str(generator.PASSPHRASE_SPECIALS)

# --- Constant collections (lists / frozensets) -----------------------

def test_default_wordlist(self) -> None:
assert constants.DEFAULT_WORDLIST == generator.DEFAULT_WORDLIST
Loading