upgrade: adopt V8.33.1 — final merge, our patches upstreamed natively
V8.33.0 absorbed our entire platform patch set as native features (discovery/env_for macro override/blocks.css priority/type-mapping fix/ frontend entries/locale override) and synced trade-pro from our remote; V8.33.1 adds HESU PIPE industrial-theme home fixes (1.3.1). Zero patch re-application this round — all verified native. Theme suites 35/35; full run 428 passed + same 10 known Windows env fails (FAILED set unchanged). Live: container 8.33.1-dev.1 healthy, hesupipe 192 entries intact, trade-pro build 203 pages 0 errors. Patch registry retired to archive — future upgrades are plain overlay. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
"""Isolated Chromium smoke of shipped standalone theme comparison; no real server."""
|
||||
import asyncio,json
|
||||
from pathlib import Path
|
||||
from playwright.async_api import async_playwright
|
||||
ROOT=Path(__file__).resolve().parents[1];REPORT=ROOT/'reports/v833';REPORT.mkdir(parents=True,exist_ok=True)
|
||||
async def main():
|
||||
async with async_playwright() as p:
|
||||
browser=await p.chromium.launch(headless=True,executable_path='/usr/bin/chromium',args=['--no-sandbox','--disable-dev-shm-usage'])
|
||||
records=[]
|
||||
for device,width in (('desktop',1365),('phone',390)):
|
||||
page=await browser.new_page(viewport={'width':width,'height':850},device_scale_factor=1)
|
||||
errs=[];page.on('pageerror',lambda e:errs.append(str(e)))
|
||||
html=(ROOT/'examples/V8_33_五套主题真实页面_离线交互预览.html').read_text(encoding='utf-8')
|
||||
await page.set_content(html,wait_until='load',timeout=60000)
|
||||
await page.select_option('#theme','trade-pro')
|
||||
await page.select_option('#page','product')
|
||||
await page.frame_locator('#preview').locator('.sf-tp-product-hero').wait_for(state='visible',timeout=12000)
|
||||
await page.wait_for_timeout(1600)
|
||||
if device=='phone':await page.locator('[data-width="390px"]').click()
|
||||
title=await page.title()
|
||||
count=await page.frame_locator('#preview').locator('.pro-trade-pro').count()
|
||||
overflow=await page.evaluate('document.documentElement.scrollWidth>window.innerWidth')
|
||||
img=REPORT/f'trade-pro-{device}.png'
|
||||
await page.screenshot(path=str(img),full_page=True)
|
||||
assert count>0,(device,'theme body missing')
|
||||
assert not overflow,(device,'overflow')
|
||||
assert not errs,(device,errs)
|
||||
records.append({'device':device,'width':width,'theme_body_found':count,'horizontal_overflow':overflow,'script_errors':errs,'screenshot':str(img.relative_to(ROOT))})
|
||||
await page.close()
|
||||
await browser.close()
|
||||
(REPORT/'browser-results.json').write_text(json.dumps(records,ensure_ascii=False,indent=2),encoding='utf-8')
|
||||
print('Chromium PASS:', records)
|
||||
if __name__=='__main__':asyncio.run(main())
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Chromium isolated visual check for industrial features and collection."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
from playwright.sync_api import sync_playwright
|
||||
root=Path(__file__).resolve().parents[1]
|
||||
pagehtml=(root/'examples/v8331-real-built-page.html').read_text()
|
||||
dest=root/'reports/v8331';dest.mkdir(parents=True,exist_ok=True)
|
||||
rows=[]
|
||||
with sync_playwright() as p:
|
||||
browser=p.chromium.launch(headless=True,executable_path='/usr/bin/chromium',args=['--no-sandbox','--disable-dev-shm-usage'])
|
||||
for width in [1440,820,390,375]:
|
||||
page=browser.new_page(viewport={'width':width,'height':920},device_scale_factor=1)
|
||||
errors=[]
|
||||
page.on('pageerror',lambda exc: errors.append(str(exc)))
|
||||
page.set_content(pagehtml,wait_until='load',timeout=30000)
|
||||
page.wait_for_timeout(150)
|
||||
facts=page.evaluate('''() => {
|
||||
const shell=document.querySelector('.pro-custom-content');
|
||||
const outerFeature=document.querySelector('.sf-section.sf-features');
|
||||
const outerList=document.querySelector('.sf-section.sf-collection');
|
||||
const innerFeature=document.querySelector('.sf-section.sf-features .sf-container > .sf-features');
|
||||
const innerList=document.querySelector('.sf-section.sf-collection .sf-container > .sf-collection');
|
||||
const box=el=>el?({x:Math.round(el.getBoundingClientRect().x),width:Math.round(el.getBoundingClientRect().width),display:getComputedStyle(el).display,cols:getComputedStyle(el).gridTemplateColumns}):null;
|
||||
return {bodyScroll:document.documentElement.scrollWidth,viewport:innerWidth,
|
||||
featuresOuter:box(outerFeature),listOuter:box(outerList),featuresInner:box(innerFeature),listInner:box(innerList),
|
||||
featureCount:document.querySelectorAll('.sf-feature').length,
|
||||
productCount:document.querySelectorAll('.sf-item').length,
|
||||
filterCount:document.querySelectorAll('.pro-custom-content .sf-filters').length,
|
||||
module:box(shell)};
|
||||
}''')
|
||||
assert facts['featuresOuter']['display']=='block',facts
|
||||
assert facts['listOuter']['display']=='block',facts
|
||||
assert facts['featuresInner']['display']=='grid',facts
|
||||
assert facts['listInner']['display']=='grid',facts
|
||||
assert facts['featuresInner']['width']>=min(900,width-80),facts
|
||||
assert facts['productCount']==6 and facts['featureCount']==4,facts
|
||||
assert facts['filterCount']==0,facts
|
||||
assert facts['bodyScroll']<=width+1,facts
|
||||
assert not errors,errors
|
||||
shot=dest/f'hesupipe-home-blocks-{width}.png'
|
||||
page.locator('.pro-custom-content').screenshot(path=str(shot),timeout=30000)
|
||||
rows.append({'width':width,'screenshot':shot.name,'facts':facts,'script_errors':errors})
|
||||
page.close()
|
||||
browser.close()
|
||||
(dest/'browser-results.json').write_text(json.dumps(rows,ensure_ascii=False,indent=2))
|
||||
for x in rows:print(x['width'], 'feature grid:',x['facts']['featuresInner']['cols'], 'product grid:', x['facts']['listInner']['cols'], 'filters:',x['facts']['filterCount'])
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Make a fully offline industrial-theme homepage preview from the real static builder."""
|
||||
import os,sys,json,html,re,tempfile
|
||||
from pathlib import Path
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
sys.path[:0]=[str(ROOT),str(ROOT/'tests')]
|
||||
from tests.legacy import LegacyTestClient
|
||||
from app import db,theme_store
|
||||
from app.main import app
|
||||
|
||||
def generate():
|
||||
target=ROOT/'examples'; target.mkdir(exist_ok=True)
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
db.DATA=Path(temporary);db.DB_FILE=db.DATA/'data.sqlite3';theme_store.THEME_ROOT=db.DATA/'theme_packages'
|
||||
os.environ['SITEFORGE_ALLOW_LEGACY_DIRECT_DESIGN_WRITE']='1'
|
||||
os.environ['SITEFORGE_ADMIN_PASSWORD']='demo-test-admin-secret-2026'
|
||||
os.environ['SITEFORGE_SESSION_SECRET']='demo-secret-not-for-use-outside-test-2026'
|
||||
with LegacyTestClient(app) as c:
|
||||
assert c.post('/api/auth/login',json={'username':'admin','password':os.environ['SITEFORGE_ADMIN_PASSWORD']}).status_code==200
|
||||
c.headers.update({'X-SiteForge-CSRF':c.get('/api/auth/me').json()['csrf']})
|
||||
r=c.post('/api/sites',json={'name':'HESU PIPE · 排版验证样站','theme':'industrial','domain':'https://example.test','mode':'migration','tagline':'工业管道与工程材料 · 完整产品方案'})
|
||||
assert r.status_code==201,r.text; sid=r.json()['id']
|
||||
home=next(e for e in c.get(f'/api/sites/{sid}/entries').json() if e['route']=='/')
|
||||
assert c.put(f'/api/factory/sites/{sid}/theme',json={'theme':'industrial','version':'1.3.1'}).status_code==200
|
||||
products=[('HDPE 给水管','适用于市政给水及工业供水管网。'),('双壁波纹排水管','适用于市政雨污水管网和地下排水工程。'),('PVC-U 排水管','耐腐蚀、规格丰富的塑料管材系统。'),('PPR 冷热水管','建筑冷热水输送常用管道系列。'),('钢丝网骨架复合管','适用于承压输水与工程管线系统。'),('HDPE 管道连接件','用于热熔及电熔连接的各类管件。')]
|
||||
for i,(title,summary) in enumerate(products,1):
|
||||
res=c.post(f'/api/sites/{sid}/entries',json={'type':'product','title':title,'route':f'/products/pipe-{i}/','summary':summary})
|
||||
assert res.status_code==201,res.text
|
||||
blocks=[{'id':'features-hp-v8331','type':'features','title':'核心优势','items':[
|
||||
{'title':'管道产品供应','text':'覆盖多种工程管材和配件,便于项目集中采购。'},
|
||||
{'title':'规格与质量管理','text':'根据实际应用提供产品选型和规格资料。'},
|
||||
{'title':'工程应用支持','text':'面向供水、排水及工业管网项目的应用场景。'},
|
||||
{'title':'全球交付服务','text':'从订单沟通到物流交付,提供清晰的合作流程。'},
|
||||
]}, {'id':'collection-hp-v8331','type':'collection','title':'动态内容列表 · 精选产品',
|
||||
'subtitle':'PRODUCT COLLECTION','source':'product','limit':6,'columns':3,
|
||||
'href':'/products/','button':'查看全部产品'}]
|
||||
saved=c.put(f'/api/sites/{sid}/pages/{home["id"]}/blueprint',json={'blocks':blocks,'page_template':'home'})
|
||||
assert saved.status_code==200,saved.text
|
||||
built=c.post(f'/api/sites/{sid}/builds',json={})
|
||||
assert built.status_code==200,built.text
|
||||
bid=built.json()['version']; dist=db.DATA/'builds'/sid/bid/'dist'
|
||||
assert (dist/'index.html').exists(),dist
|
||||
source=(dist/'index.html').read_text()
|
||||
# Inline exact packaged CSS/JS, so the demo is readable without a server or external requests.
|
||||
def stylesheet(m):
|
||||
path=m.group(1)
|
||||
local=dist/path
|
||||
return '<style>\n'+local.read_text(encoding='utf-8')+'\n</style>' if local.is_file() else ''
|
||||
source=re.sub(r'<link rel="stylesheet" href="(assets/[A-Za-z0-9._-]+\.css)">',stylesheet,source)
|
||||
def script(m):
|
||||
local=dist/m.group(1)
|
||||
if local.is_file():
|
||||
return '<script>\n'+local.read_text(encoding='utf-8').replace('</script','<\\/script')+'\n</script>'
|
||||
return ''
|
||||
source=re.sub(r'<script defer src="(assets/[A-Za-z0-9._-]+\.js)"></script>',script,source)
|
||||
# These paths are demonstrative and intentionally not published.
|
||||
isolated=target/'V8_33_1_HesuPipe_首页布局真实构建预览.html'
|
||||
snippet=json.dumps(source,ensure_ascii=False).replace('<','\\u003c')
|
||||
wrapper='''<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>HesuPipe · 首页核心优势与产品列表</title><style>*{box-sizing:border-box}body{margin:0;background:#eef2f5;color:#1d3344;font-family:system-ui,-apple-system,sans-serif}.bar{display:flex;gap:15px;align-items:center;justify-content:space-between;padding:14px 23px;background:#fff;border-bottom:1px solid #dce4e9;flex-wrap:wrap}.bar strong{font-size:16px}.bar small{display:block;font-size:11px;color:#657a89;margin-top:4px}.buttons{display:flex;gap:6px}.buttons button{border:1px solid #cbdce4;background:#f6f9fa;padding:9px 14px;border-radius:6px;font-weight:650;color:#1e4359;cursor:pointer}.buttons button[aria-pressed=true]{background:#0f5e78;color:#fff}iframe{border:0;width:100%;height:calc(100vh - 74px);min-height:550px;background:#fff;display:block;margin:auto;transition:width .15s}.note{position:fixed;right:20px;bottom:18px;border:1px solid #d0dce3;border-radius:9px;background:#fff;padding:10px 13px;font-size:12px;box-shadow:0 4px 15px #0001}</style></head><body><div class="bar"><div><strong>HESU PIPE · 核心优势 / 动态内容列表</strong><small>V8.33.1 从实际静态构建生成的离线页面 · 示范文字与占位产品图</small></div><div class="buttons"><button data-size="100%" aria-pressed="true">电脑版</button><button data-size="820px" aria-pressed="false">平板</button><button data-size="390px" aria-pressed="false">手机版</button></div></div><iframe title="工业建材主题首页实际构建预览" sandbox="allow-scripts" id="preview"></iframe><span class="note">仅检查预览 · 不连接线上网站</span><script id="payload" type="application/json">PAYLOAD</script><script>const raw=JSON.parse(document.getElementById('payload').textContent);const target=document.getElementById('preview');target.srcdoc=raw;document.querySelectorAll('[data-size]').forEach(btn=>btn.addEventListener('click',()=>{target.style.width=btn.dataset.size;document.querySelectorAll('[data-size]').forEach(item=>item.setAttribute('aria-pressed',String(item===btn)));}));</script></body></html>'''
|
||||
isolated.write_text(wrapper.replace('PAYLOAD',snippet),encoding='utf-8')
|
||||
(target/'v8331-real-built-page.html').write_text(source,encoding='utf-8')
|
||||
print('generated',isolated,'bytes',isolated.stat().st_size,'build',bid)
|
||||
if __name__=='__main__':generate()
|
||||
@@ -0,0 +1,78 @@
|
||||
"""End-to-end HTTP + Worker smoke test on an isolated disposable SQLite database.
|
||||
|
||||
Launches an actual uvicorn process, calls authenticated HTTP APIs, runs a real
|
||||
Worker claim + isolated subprocess build, then downloads and checks the archive.
|
||||
Never points at production data or remote deployment services.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from urllib.error import URLError
|
||||
from urllib.request import urlopen
|
||||
import httpx
|
||||
|
||||
ROOT=Path(__file__).resolve().parent.parent
|
||||
|
||||
def run():
|
||||
with tempfile.TemporaryDirectory(prefix='siteforge-e2e-') as folder:
|
||||
with socket.socket() as sock:
|
||||
sock.bind(('127.0.0.1',0)); port=sock.getsockname()[1]
|
||||
base=f'http://127.0.0.1:{port}'
|
||||
env=os.environ.copy()
|
||||
env.update(SITEFORGE_ADMIN_PASSWORD='e2e-only-dummy-password-123456',
|
||||
SITEFORGE_SESSION_SECRET='e2e-only-session-secret-long-value-123456',
|
||||
SITEFORGE_DATA_DIR=folder,SITEFORGE_DB_FILE=str(Path(folder)/'siteforge.sqlite3'),
|
||||
SITEFORGE_DATABASE_URL='',SITEFORGE_REDIS_URL='',SITEFORGE_COOKIE_SECURE='false')
|
||||
proc=subprocess.Popen([sys.executable,'-m','uvicorn','app.main:app','--host','127.0.0.1','--port',str(port)],
|
||||
cwd=ROOT,env=env,stdout=subprocess.DEVNULL,stderr=subprocess.PIPE)
|
||||
try:
|
||||
for n in range(120):
|
||||
if proc.poll() is not None:raise RuntimeError('Web startup failed: '+proc.stderr.read().decode(errors='replace')[-3000:])
|
||||
try:
|
||||
with urlopen(base+'/api/ready',timeout=0.5) as result:
|
||||
if result.status==200:break
|
||||
except (OSError,URLError):time.sleep(0.1)
|
||||
else:raise RuntimeError('Web readiness timeout')
|
||||
with httpx.Client(base_url=base,timeout=20,follow_redirects=False) as client:
|
||||
auth=client.post('/api/auth/login',json={'username':'admin','password':env['SITEFORGE_ADMIN_PASSWORD']})
|
||||
assert auth.status_code==200,auth.text
|
||||
me=client.get('/api/auth/me')
|
||||
assert me.status_code==200,me.text
|
||||
client.headers['X-SiteForge-CSRF']=me.json()['csrf']
|
||||
site=client.post('/api/sites',json={'name':'V8.33 Trade Pro Worker','theme':'trade-pro'})
|
||||
assert site.status_code==201,site.text
|
||||
sid=site.json()['id']
|
||||
page=client.post(f'/api/sites/{sid}/entries',json={'type':'page','title':'Test about','route':'/about/','body':'E2E smoke content'})
|
||||
assert page.status_code==201,page.text
|
||||
job=client.post(f'/api/sites/{sid}/builds',json={'mirror_images':False},headers={'Idempotency-Key':'e2e-build-000001'})
|
||||
assert job.status_code==202,job.text
|
||||
completed=subprocess.run([sys.executable,'-c','from app import db,enterprise,worker; db.init(); enterprise.setup(); assert worker.run_once()'],cwd=ROOT,env=env,capture_output=True,text=True,timeout=70)
|
||||
assert completed.returncode==0,completed.stderr[-2500:]
|
||||
builds=client.get(f'/api/sites/{sid}/builds')
|
||||
assert builds.status_code==200,builds.text
|
||||
done=[x for x in builds.json() if x['status']=='complete']
|
||||
if not done:
|
||||
raise AssertionError('No completed static build: '+str(builds.json())[:1200])
|
||||
archive=client.get(f"/api/sites/{sid}/builds/{done[0]['id']}/download")
|
||||
assert archive.status_code==200,archive.text[:300]
|
||||
with zipfile.ZipFile(io.BytesIO(archive.content)) as z:
|
||||
assert 'index.html' in z.namelist()
|
||||
assert 'about/index.html' in z.namelist()
|
||||
assert '<html lang="en"' in z.read('index.html').decode('utf-8')
|
||||
preview=client.get(f"/preview/{sid}/{done[0]['id']}/about/")
|
||||
assert preview.status_code==200 and 'E2E smoke content' in preview.text
|
||||
print('PASS: ready -> login/CSRF -> create site/page -> enqueue -> Worker build -> ZIP -> preview')
|
||||
finally:
|
||||
proc.terminate()
|
||||
try:proc.communicate(timeout=5)
|
||||
except subprocess.TimeoutExpired:proc.kill();proc.communicate()
|
||||
|
||||
if __name__=='__main__':run()
|
||||
@@ -0,0 +1,28 @@
|
||||
"""V8.29 isolated regression with per-batch deadline and raw logs."""
|
||||
import argparse,datetime,json,subprocess,sys,time
|
||||
from pathlib import Path
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
def main():
|
||||
ap=argparse.ArgumentParser();ap.add_argument('--timeout',type=int,default=90);ap.add_argument('--group-size',type=int,default=3);ap.add_argument('--start',type=int,default=1);ap.add_argument('--end',type=int,default=999);args=ap.parse_args()
|
||||
all_files=sorted((ROOT/'tests').glob('test_*.py'))
|
||||
groups=[all_files[i:i+args.group_size] for i in range(0,len(all_files),args.group_size)]
|
||||
dest=ROOT/'reports/v833-regression';dest.mkdir(parents=True,exist_ok=True)
|
||||
outcomes=[]
|
||||
for index,files in enumerate(groups,1):
|
||||
if not args.start <= index <= args.end:continue
|
||||
start=time.monotonic();command=[sys.executable,'-m','pytest','-q',*[str(f.relative_to(ROOT)) for f in files]]
|
||||
try:
|
||||
proc=subprocess.run(command,cwd=ROOT,capture_output=True,text=True,timeout=args.timeout)
|
||||
code=proc.returncode;output=proc.stdout+'\n'+proc.stderr;status='passed' if code==0 else 'failed'
|
||||
except subprocess.TimeoutExpired as e:
|
||||
output=(e.stdout or b'').decode(errors='replace') if isinstance(e.stdout,bytes) else (e.stdout or '')
|
||||
output+='\nTIMEOUT exceeded';status='timeout'
|
||||
elapsed=round(time.monotonic()-start,2)
|
||||
(dest/f'batch-{index:02}.log').write_text(output,encoding='utf-8')
|
||||
outcomes.append({'batch':index,'files':[f.name for f in files],'status':status,'seconds':elapsed,'summary':output[-350:]})
|
||||
print(f'[{index:02}/{len(groups)}] {status.upper()} {elapsed:.1f}s: '+', '.join(f.name for f in files),flush=True)
|
||||
result={'version':'8.33.1-dev.1','generated_utc':datetime.datetime.now(datetime.timezone.utc).isoformat(),'groups':outcomes,'all_passed':all(x['status']=='passed' for x in outcomes)}
|
||||
(dest/'result.json').write_text(json.dumps(result,ensure_ascii=False,indent=2),encoding='utf-8')
|
||||
print('RESULT:',result['all_passed'],sum(x['status']=='passed' for x in outcomes),len(groups),flush=True)
|
||||
return 0 if result['all_passed'] else 1
|
||||
if __name__=='__main__':raise SystemExit(main())
|
||||
@@ -6,7 +6,7 @@ import sys
|
||||
ROOT=Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0,str(ROOT))
|
||||
from app.version import VERSION
|
||||
assert VERSION=='8.32.0-dev.1'
|
||||
assert VERSION=='8.33.1-dev.1'
|
||||
api=(ROOT/'app'/'main.py').read_text(encoding='utf-8')
|
||||
assert 'return builder.build(' not in api, 'A synchronous request-thread build has reappeared'
|
||||
assert "@app.post('/api/sites/{site_id}/builds', status_code=202)" in api
|
||||
@@ -22,4 +22,4 @@ for file in (ROOT/'requirements.lock',ROOT/'requirements-dev.lock'):
|
||||
assert (ROOT/'.github/workflows/ci.yml').is_file()
|
||||
for file in (ROOT/'app/static').glob('*.js'):
|
||||
subprocess.run(['node','--check',str(file)],check=True)
|
||||
print('PASS: version, 4× themes, worker route, pinned dependencies, CI workflow, JS syntax')
|
||||
print('PASS: version, 5× themes, worker route, pinned dependencies, CI workflow, JS syntax')
|
||||
|
||||
Reference in New Issue
Block a user