upgrade: adopt V8.21.3 (V8.13→8.21.3: P5 SFTP/S3, P6 multilingual, editor overhaul, enterprise hardening)

Overlay ~150 upstream items onto our V8.12 base, preserving trade-pro
theme, registry tests, and design docs. Re-applied our patch set
(discovery/env_for/blocks.css/type-mapping/theme_color/frontend entries,
test_theme_pro_v3 mods) — all anchors survived upstream evolution.

New platform enhancement (fixes upgrade-introduced regression):
multilingual.site_locale now honors settings_json.locale, plus
PUT /api/i18n/sites/{id}/locale — standalone English sites get
<html lang='en'> under the V8.15 locale-rewrite pipeline.

compose: adopt V8.21.3 (image 8.21.3-dev.1, /api/ready probes, MySQL/S3
env) with project name pinned to siteforge-v8-10 (data volumes).

Tests: 356 passed / 11 failed — all 11 confirmed pre-existing in
pristine V8.21.3 on this host (10 upstream Windows env + our build test
now green after locale fix). Patch registry updated in plan doc.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-23 12:31:03 +08:00
parent 257c89e159
commit 601451f22b
218 changed files with 10441 additions and 628 deletions
+120
View File
@@ -0,0 +1,120 @@
"""V8.19 unified console contract; server permissions remain authoritative."""
from app.version import VERSION
import json
import pytest
from tests.test_enterprise import root,site
from scripts.install_into_art_design_pro import install, transform_routes
def test_bootstrap_admin_uses_one_navigation_and_no_secrets(root):
one=site(root,'HESU A');two=site(root,'HESU B')
r=root.get('/api/console/bootstrap')
assert r.status_code==200,r.text
info=r.json()
assert info['version']==VERSION
assert {x['id'] for x in info['sites']}=={one,two}
assert info['selected_site_id'] in {one,two}
assert info['capabilities']['create_site'] and info['capabilities']['deploy']
assert [i['key'] for i in info['navigation']]==['dashboard','sites','content','design','migration','release','system']
assert 'csrf' not in json.dumps(info).lower()
assert root.get('/api/console/bootstrap?site_id=not-a-site').status_code==403
def test_scope_does_not_disclose_foreign_site_and_rejects_unassigned(root):
one=site(root,'Assigned');two=site(root,'Not assigned')
uid=root.post('/api/enterprise/team/users',json={
'username':'v819view','display_name':'Viewer','password':'viewer-strong-password-2026'}).json()['id']
assert root.put(f'/api/enterprise/sites/{one}/members',json={'user_id':uid,'role':'viewer'}).status_code==200
root.post('/api/auth/logout')
assert root.post('/api/auth/login',json={'username':'v819view','password':'viewer-strong-password-2026'}).status_code==200
root.headers['X-SiteForge-CSRF']=root.get('/api/auth/me').json()['csrf']
r=root.get('/api/console/bootstrap')
assert r.status_code==200,r.text
b=r.json()
assert [s['id'] for s in b['sites']]==[one]
assert b['capabilities']['role']=='viewer'
assert not b['capabilities']['edit'] and not b['capabilities']['deploy']
assert not b['capabilities']['manage_users']
assert all(item['key']!='system' for item in b['navigation'])
assert root.get(f'/api/console/bootstrap?site_id={two}').status_code==403
assert root.get(f'/api/sites/{two}/entries').status_code==403
assert root.get(f'/api/console/bootstrap?site_id={one}').status_code==200
def test_empty_site_roles_still_have_dashboard(root):
resp=root.get('/api/console/bootstrap')
assert resp.status_code==200
nav=resp.json()['navigation']
assert nav[0]['key']=='dashboard'
assert all(n['key'] not in {'content','design','migration','release'} for n in nav)
def fixture(tmp_path):
repo=tmp_path/'art';route=repo/'src/router/routes/asyncRoutes.ts'
route.parent.mkdir(parents=True,exist_ok=True)
route.write_text('export const asyncRoutes: MenuListType[] = [\n{path:"/unrelated",name:"Safe"},\n];\n')
(repo/'package.json').write_text('{"name":"art-design-pro"}')
return repo,route
def test_art_installer_single_menu_and_safe_backup(tmp_path):
repo,route=fixture(tmp_path)
original=route.read_text()
result=install(repo,dry_run=True)
assert result['dry_run'] and not result['conflicting_views']
assert route.read_text()==original
install(repo,dry_run=False)
updated=route.read_text()
assert updated.count("path: '/siteforge'")==1
assert "path: 'production'" in updated
assert updated.count('// SITEFORGE_FACTORY_V8_REGISTERED')==1
assert 'Safe' in updated
assert install(repo,verify=True)['installed']
assert route.with_name('asyncRoutes.ts.siteforge-backup').read_text()==original
install(repo,dry_run=False)
assert route.read_text()==updated
def test_installer_fails_closed_on_conflicting_views(tmp_path):
repo,route=fixture(tmp_path)
target=repo/'src/views/siteforge/ConsoleHome.vue'
target.parent.mkdir(parents=True)
target.write_text('someone else code')
with pytest.raises(ValueError,match='拒绝覆盖'):
install(repo,dry_run=False)
assert 'SITEFORGE' not in route.read_text()
install(repo,dry_run=False,overwrite_views=True)
assert target.with_name('ConsoleHome.vue.siteforge-backup').read_text()=='someone else code'
target.write_text('broken')
with pytest.raises(ValueError,match='不匹配'):
install(repo,verify=True)
def test_installer_rejects_partial_routes(tmp_path):
repo,route=fixture(tmp_path)
route.write_text('export const asyncRoutes = [ /* SITEFORGE_V819_ROUTES_BEGIN */ ]')
with pytest.raises(ValueError,match='标记不完整'):
install(repo,dry_run=False)
def test_legacy_upgrade_requires_explicit_confirmation(tmp_path):
repo,route=fixture(tmp_path)
route.write_text('export const asyncRoutes: any[] = [\n // SITEFORGE_FACTORY_V8_REGISTERED\n {path:"/siteforge/production",meta:{a:1}},\n // SITEFORGE_FACTORY_V7_REGISTERED\n {path:"/siteforge",children:[{path:"factory",name:"A"}]},\n {path:"/unrelated"},\n]')
with pytest.raises(ValueError,match='--migrate-legacy'):
install(repo,dry_run=False)
install(repo,dry_run=False,migrate_legacy=True)
routes=route.read_text()
assert routes.count("path: '/siteforge'")==1
assert 'unrelated' in routes
assert routes.count('// SITEFORGE_FACTORY_V8_REGISTERED')==1
def test_shared_sdk_and_vue_pages_exist():
from pathlib import Path
base=Path(__file__).resolve().parents[1]/'integrations/art-design-pro/src/views/siteforge'
assert (base/'_shared.ts').is_file()
assert 'X-SiteForge-CSRF' in (base/'_shared.ts').read_text()
for name in ('ConsoleHome.vue','ContentCenter.vue'):
text=(base/name).read_text()
assert '<template>' in text and "from './_shared'" in text
assert 'POST' in (base/'ContentCenter.vue').read_text() or 'postJson' in (base/'ContentCenter.vue').read_text()