Skip to content

fix(static): nie fabrykuj hashy bez manifestu w TolerantManifestStaticFilesStorage#276

Merged
mpasternak merged 1 commit into
devfrom
worktree-static-test-fix
Jun 1, 2026
Merged

fix(static): nie fabrykuj hashy bez manifestu w TolerantManifestStaticFilesStorage#276
mpasternak merged 1 commit into
devfrom
worktree-static-test-fix

Conversation

@mpasternak
Copy link
Copy Markdown
Member

Problem

Po zmergowaniu #272 testy pbn_import/tests/test_admin_compression.py (2×) i
import_list_if/tests/test_views.py (2×) padały lokalnie, choć przechodziły
na CI
:

UncompressableFileError: 'pbn_import/css/admin.8a70fd13d01d.css' could not be found
AttributeError: 'WSGIRequest' object has no attribute 'user'   # webtest klika martwy /static/...przyklad.<hash>.xlsx

Root cause

TolerantManifestStaticFilesStorage ma manifest_strict=False. Gdy nie ma
manifestu
(staticfiles.json), vanilla ManifestFilesMixin.stored_name
liczy content-hash w locie dla każdego pliku leżącego w STATIC_ROOT i
zwraca name.<hash>.ext — URL do pliku, którego fizycznej kopii nikt nigdy nie
wygenerował (stary collectstatic zrobił tylko nie-hashowaną kopię).

  • Lokalnie: developer ma zalegający staticroot sprzed wprowadzenia
    Manifestu (same nie-hashowane pliki, bez staticfiles.json) → martwe URL-e →
    compressor UncompressableFileError, webtest 404 (request.user).
  • CI: test-runner ma pusty STATIC_ROOT (grunt build-non-interactive
    pomija shell:collectstatic) → hashed_name rzuca ValueError → łapane →
    nazwa nie-hashowana → przechodzi.
  • Pod pytest DEBUG=False, więc short-circuit DEBUG w HashedFilesMixin.url()
    (który w runserver zwróciłby nazwę nie-hashowaną) nie ratuje.

Czyli zachowanie testów zależało od przypadkowego stanu lokalnego staticroot
nie-hermetyczne.

Fix

Override stored_name: gdy hashed_files jest puste (brak manifestu) zwróć
nazwę oryginalną zamiast fabrykować martwy hash. Manifest jest jedynym
źródłem prawdy dla hashy; bez niego realny plik źródłowy zaserwuje
finder/runserver/whitenoise.

def stored_name(self, name):
    if not self.hashed_files:        # brak manifestu (dev/test) → nazwa oryginalna
        return name
    return super().stored_name(name) # manifest jest → vanilla, cache-busting 1:1

Gating na pusty manifest jest celowo wąski:

  • Produkcja (.baked po collectstatic) ma pełny manifest → hashed_files
    niepuste → delegujemy do vanilla → cache-busting długiego ogona działa 1:1.
  • Build-time post_process używa osobnej ścieżki (_stored_name, force=True),
    więc ten override jej nie dotyka.

collectstatic zostaje wyłącznie w build-time (nie dokładamy go do testów — CI
celowo go nie odpala, byłoby wolne i kruche).

Weryfikacja

  • TDD unit (RED→GREEN): test_existing_file_without_manifest_is_not_hashed_on_the_fly
    — przed fixem zwracał foo.<hash>.css, po fixie foo.css; kontrast z vanilla
    dowodzi że to nasza subklasa daje wynik. Plus test_present_manifest_still_hashes
    (guard produkcyjny). 7 passed.
  • Integracja w dokładnym warunku buga: odtworzony stan użytkownika
    (collectstatic → usunięty manifest + 1640 hashowanych kopii, zostały same
    nie-hashowane), 4 wcześniej padające testy → 4 passed.
  • ruff: czysto.

🤖 Generated with Claude Code

…cFilesStorage

Follow-up do #272/#269. Bez `staticfiles.json` (dev/testy bez collectstatic,
albo stary `staticroot` sprzed Manifestu) vanilla `ManifestFilesMixin.stored_name`
z `manifest_strict=False` liczył content-hash w locie dla pliku leżącego w
STATIC_ROOT → zwracał `name.<hash>.ext`, czyli URL do pliku, którego fizycznej
kopii nikt nie wygenerował. Efekt lokalnie (pod pytest `DEBUG=False`, więc
short-circuit DEBUG w `.url()` nie ratuje): django-compressor rzucał
`UncompressableFileError`, a bezpośredni GET (webtest) dostawał 404 —
`test_admin_compression` i `import_list_if/test_views` padały. Na CII przechodziło,
bo test-runner ma pusty STATIC_ROOT (`grunt build-non-interactive` pomija
collectstatic) → `hashed_name` rzucał `ValueError` → łapane → nazwa nie-hashowana.

Override `stored_name`: gdy `hashed_files` jest puste (brak manifestu) zwróć
nazwę oryginalną zamiast martwego hasha. Gating na pusty manifest jest wąski —
gdy collectstatic wygenerował manifest (produkcja, `.baked`), delegujemy do
vanilla, więc cache-busting długiego ogona działa 1:1. Build-time `post_process`
używa osobnej ścieżki (`_stored_name`), więc override go nie dotyka.

Testy: regresja (plik w STATIC_ROOT + brak manifestu → nazwa oryginalna, kontrast
z vanilla fabrykującym hash) + guard produkcyjny (manifest obecny → nadal hash).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mpasternak mpasternak merged commit b4b1ee6 into dev Jun 1, 2026
8 checks passed
@mpasternak mpasternak deleted the worktree-static-test-fix branch June 1, 2026 11:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant