diff --git a/app/core.py b/app/core.py index 93107de..71b2b3b 100644 --- a/app/core.py +++ b/app/core.py @@ -13,8 +13,12 @@ ROOT = Path(__file__).resolve().parent.parent def discover_themes(): """Discover built-in themes from themes/*/manifest.json (single source of truth).""" + themes_dir = ROOT / 'themes' + if not themes_dir.is_dir(): + raise RuntimeError(f'themes directory missing: {themes_dir}') found = [] - for entry in sorted((ROOT / 'themes').iterdir()): + for entry in sorted(themes_dir.iterdir()): + # Underscore prefix marks non-theme assets (e.g. _professional_components.html). if not entry.is_dir() or entry.name.startswith('_'): continue try: diff --git a/docs/superpowers/plans/2026-10-22-trade-pro-theme-port.md b/docs/superpowers/plans/2026-10-22-trade-pro-theme-port.md index 8c8d3be..817b5cc 100644 --- a/docs/superpowers/plans/2026-10-22-trade-pro-theme-port.md +++ b/docs/superpowers/plans/2026-10-22-trade-pro-theme-port.md @@ -12,6 +12,7 @@ - 工作目录:`C:\Users\30997\Downloads\SiteForge_Factory_V5_Web_Docker\SiteForge_Enterprise_V8_10`(下称 `$ROOT`,bash 里用正斜杠) - Docker 实例已运行:`http://127.0.0.1:8787`(容器 siteforge-v8-10-siteforge-1/-worker-1),admin 密码在 `.env` - 测试命令:`python -m pytest -q`(当前 210 项全绿,任何改动后必须保持);JS 检查 `node --check app/static/.js` +- **宿主机快速循环**(V8_12 起):`PYTHONUTF8=1 .venv/Scripts/python -m pytest -q`,基线 **227 passed + 2 known-fail**(test_p0_backup.py::test_sqlite_backup_and_restore_with_media、test_p0_p4_completion_v89.py::test_restore_replaces_directory_atomically_and_preserves_previous——均为 Windows sqlite 文件锁环境问题,改动前即失败);任何新失败都是回归。权威全量验证用容器:`MSYS_NO_PATHCONV=1 docker run --rm -v "$(pwd -W):/project" -w /project python:3.12-slim sh -c "python -m pip install -q -r requirements-dev.lock && python -m pytest -q"`(容器基线 226 passed + 1 node 环境失败) - cms 设计源:`D:/project/cms/runtimes/astro-site-runtime/src`(tokens.css 250 行、base.css 445 行、components/*.astro、pages/[locale]/*.astro);像素基准 `dist/hesupipe/en/` - 改动 Python 后需 `docker compose up -d --build` 重建容器再验证 HTTP 层(构建约 1 分钟,有缓存) - 契约清单已固化于 spec §2(F1-F13),本计划直接引用 diff --git a/tests/test_theme_pro_v3.py b/tests/test_theme_pro_v3.py index ee711ed..dde6d31 100644 --- a/tests/test_theme_pro_v3.py +++ b/tests/test_theme_pro_v3.py @@ -6,8 +6,7 @@ import zipfile from pathlib import Path from bs4 import BeautifulSoup from test_factory_v2 import factory, site, zip_theme - -THEMES=('automotive','industrial','corporate','minimal') +from app.core import THEMES def test_four_complete_theme_families_have_distinct_markup(factory): main=[] diff --git a/tests/test_theme_registry.py b/tests/test_theme_registry.py index 6070702..cc079ac 100644 --- a/tests/test_theme_registry.py +++ b/tests/test_theme_registry.py @@ -14,5 +14,26 @@ def test_discover_returns_builtin_themes_with_manifests(): assert manifest.get('id') == tid def test_three_whitelists_agree(): + # Tripwire: the three names must stay the same object. Fails only if someone + # reintroduces a hardcoded copy of the list anywhere. assert set(core.THEMES) == set(theme_pro.THEMES) == set(theme_store.BUILTIN) assert isinstance(core.THEMES, tuple) + +def test_discover_skips_broken_theme_dirs(tmp_path, monkeypatch): + themes = tmp_path / 'themes' + good = themes / 'good-one'; good.mkdir(parents=True) + (good / 'manifest.json').write_text(json.dumps({'id': 'good-one'}), encoding='utf-8') + broken = themes / 'broken'; broken.mkdir() + (broken / 'manifest.json').write_text('{not json', encoding='utf-8') + mismatch = themes / 'mismatch'; mismatch.mkdir() + (mismatch / 'manifest.json').write_text(json.dumps({'id': 'other-name'}), encoding='utf-8') + no_manifest = themes / 'no-manifest'; no_manifest.mkdir() + underscore = themes / '_shared'; underscore.mkdir() + monkeypatch.setattr(core, 'ROOT', tmp_path) + assert core.discover_themes() == ('good-one',) + +def test_discover_fails_fast_when_themes_dir_missing(tmp_path, monkeypatch): + monkeypatch.setattr(core, 'ROOT', tmp_path) + import pytest + with pytest.raises(RuntimeError, match='themes directory missing'): + core.discover_themes()