feat(I6): FE/BE enum contract test — pin JS mirrors to backend canon

The plain-JS frontend re-declares two backend enum value-sets by hand:
download-event statuses (downloadStatus.js) and platform keys
(platformColor.js). With no TS codegen, drift is silent — the same class
as the last_verified_at field mismatch.

tests/test_fe_be_contract.py parses both JS mirrors and asserts they equal
the backend canon (downloads._ALLOWED_STATUSES, platforms.known_platform_keys())
so a change on either side fails CI on whichever moved. Backend is the
source of truth. Documented the invariant in both JS file headers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-29 15:34:20 -04:00
parent 8649a13118
commit 08420cd619
3 changed files with 49 additions and 2 deletions
+2 -1
View File
@@ -1,7 +1,8 @@
// Single source of truth for the download-event status enum -> display // Single source of truth for the download-event status enum -> display
// metadata (label/color/icon). Mirrors the backend ENUM // metadata (label/color/icon). Mirrors the backend ENUM
// (pending|running|ok|error|skipped). Used by the stat chips, the filter // (pending|running|ok|error|skipped). Used by the stat chips, the filter
// dropdown, and the event rows so they never drift apart. // dropdown, and the event rows so they never drift apart. The value set is
// pinned against backend _ALLOWED_STATUSES by tests/test_fe_be_contract.py.
export const DOWNLOAD_STATUSES = [ export const DOWNLOAD_STATUSES = [
{ value: 'pending', label: 'Queued', color: 'grey', icon: 'mdi-clock-outline' }, { value: 'pending', label: 'Queued', color: 'grey', icon: 'mdi-clock-outline' },
+3 -1
View File
@@ -1,7 +1,9 @@
// Single source of truth for platform → color + icon mapping. Used by // Single source of truth for platform → color + icon mapping. Used by
// PlatformChip and any other GS-style platform-tagged surface. The six // PlatformChip and any other GS-style platform-tagged surface. The six
// platforms FC supports map 1:1 to the GS palette; unknown platforms fall // platforms FC supports map 1:1 to the GS palette; unknown platforms fall
// back to grey + mdi-web. Operator-confirmed scope 2026-05-27. // back to grey + mdi-web. Operator-confirmed scope 2026-05-27. The ICONS key
// set is pinned against backend known_platform_keys() by
// tests/test_fe_be_contract.py.
const ICONS = { const ICONS = {
patreon: 'mdi-patreon', patreon: 'mdi-patreon',
+44
View File
@@ -0,0 +1,44 @@
"""FE/BE enum contract — guards against the field/enum drift class.
FabledCurator's frontend is plain JS (no TS codegen), so the frontend
re-declares a few backend enum value-sets by hand:
- download-event statuses → frontend/src/utils/downloadStatus.js
- platform keys → frontend/src/utils/platformColor.js
If either side edits its list without the other, the UI silently
mislabels rows or drops a platform (the `last_verified_at`-style drift).
These tests parse the JS mirrors and assert they equal the backend's
canonical definitions, so any divergence fails CI on whichever side moved.
The backend definitions are the source of truth; update the JS to match.
"""
import re
from pathlib import Path
from backend.app.api.downloads import _ALLOWED_STATUSES
from backend.app.services.platforms import known_platform_keys
_FRONTEND = Path(__file__).resolve().parents[1] / "frontend" / "src" / "utils"
def _read(name: str) -> str:
return (_FRONTEND / name).read_text(encoding="utf-8")
def test_download_status_values_match_backend() -> None:
# downloadStatus.js: each entry is `{ value: 'pending', ... }`
fe_values = set(re.findall(r"value:\s*'([a-z]+)'", _read("downloadStatus.js")))
assert fe_values == set(_ALLOWED_STATUSES), (
"downloadStatus.js DOWNLOAD_STATUSES is out of sync with backend "
"downloads._ALLOWED_STATUSES"
)
def test_platform_keys_match_backend() -> None:
# platformColor.js ICONS: each entry is `patreon: 'mdi-patreon',`
fe_keys = set(re.findall(r"^\s*(\w+):\s*'mdi-", _read("platformColor.js"), re.M))
assert fe_keys == set(known_platform_keys()), (
"platformColor.js ICONS is out of sync with backend "
"platforms.known_platform_keys()"
)