chore: baseline V8.10.0-dev.1 with v8_workflows type-mapping fix and trade-pro spec
- SiteForge Enterprise V8.10.0-dev.1 upstream source (210 tests) - Fix: v8_workflows._mapped_input now reads both 类型/分类 columns so WXR imports keep post/product types instead of collapsing to page - Docs: trade-pro theme port spec (R3) and implementation plan Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
"""New V6 async contracts: these tests do not use legacy sync adapters."""
|
||||
import json
|
||||
from fastapi.testclient import TestClient
|
||||
import pytest
|
||||
from app import db,builder,jobs,worker,enterprise
|
||||
from app.main import app
|
||||
|
||||
@pytest.fixture
|
||||
def web(tmp_path,monkeypatch):
|
||||
monkeypatch.setattr(db,'DATA',tmp_path)
|
||||
monkeypatch.setattr(db,'DB_FILE',tmp_path/'siteforge.sqlite3')
|
||||
monkeypatch.setenv('SITEFORGE_ADMIN_PASSWORD','test-admin-password-for-queue')
|
||||
monkeypatch.setenv('SITEFORGE_SESSION_SECRET','test-session-secret-for-queue-2026-much-longer')
|
||||
with TestClient(app) as c:
|
||||
assert c.post('/api/auth/login',json={'password':'test-admin-password-for-queue'}).status_code==200
|
||||
c.headers['X-SiteForge-CSRF']=c.get('/api/auth/me').json()['csrf']
|
||||
sid=c.post('/api/sites',json={'name':'Queued Site','theme':'corporate'}).json()['id']
|
||||
yield c,sid
|
||||
|
||||
def test_api_enqueue_idempotency_cancel_and_version(web):
|
||||
c,sid=web
|
||||
from app.version import VERSION
|
||||
assert c.get('/api/health').json()['version']==VERSION
|
||||
key='same-request-unique-123'
|
||||
r=c.post(f'/api/sites/{sid}/builds',json={'mirror_images':False},headers={'Idempotency-Key':key})
|
||||
assert r.status_code==202,r.text
|
||||
j=r.json()['id']
|
||||
duplicate=c.post(f'/api/sites/{sid}/builds',json={'mirror_images':False},headers={'Idempotency-Key':key})
|
||||
assert duplicate.json()['id']==j and duplicate.json()['existing']
|
||||
conflict=c.post(f'/api/sites/{sid}/builds',json={'mirror_images':True},headers={'Idempotency-Key':key})
|
||||
assert conflict.status_code==409
|
||||
cancelling=c.post(f'/api/enterprise/sites/{sid}/jobs/{j}/cancel')
|
||||
assert cancelling.status_code==200 and cancelling.json()['state']=='cancelled'
|
||||
assert worker.run_once(executor=lambda sid,options:builder.build(sid)) is None
|
||||
assert c.get(f'/api/enterprise/sites/{sid}/jobs/{j}').json()['state']=='cancelled'
|
||||
|
||||
def test_only_worker_generates_static_build(web):
|
||||
c,sid=web
|
||||
start=c.get(f'/api/sites/{sid}/builds').json()
|
||||
submitted=c.post(f'/api/sites/{sid}/builds',json={})
|
||||
assert submitted.status_code==202
|
||||
assert c.get(f'/api/sites/{sid}/builds').json()==start
|
||||
job=submitted.json()['id']
|
||||
assert worker.run_once(executor=lambda sid,opts:builder.build(sid,bool(opts.get('mirror_images',False))))==job
|
||||
record=c.get(f'/api/enterprise/sites/{sid}/jobs/{job}').json()
|
||||
assert record['state']=='done',record['error']
|
||||
version=json.loads(record['result_json'])['version']
|
||||
assert c.get(f'/preview/{sid}/{version}/').status_code==200
|
||||
|
||||
def test_failed_job_retries_and_lease_expiry(web):
|
||||
c,sid=web
|
||||
job=c.post(f'/api/sites/{sid}/builds',json={}).json()['id']
|
||||
attempts=[]
|
||||
def fail_once(sid,opts):
|
||||
attempts.append(sid)
|
||||
if len(attempts)==1:raise TimeoutError('transient timeout')
|
||||
return {'version':'ok','pages':1}
|
||||
worker.run_once(executor=fail_once)
|
||||
row=db.one('SELECT * FROM build_jobs WHERE id=?',(job,))
|
||||
assert row['state']=='retrying' and row['attempts']==1
|
||||
db.execute('UPDATE build_jobs SET available_at=? WHERE id=?',(db.now(),job))
|
||||
worker.run_once(executor=fail_once)
|
||||
assert db.one('SELECT state,attempts FROM build_jobs WHERE id=?',(job,))=={'state':'done','attempts':2}
|
||||
second=c.post(f'/api/sites/{sid}/builds',json={}).json()['id']
|
||||
db.execute("UPDATE build_jobs SET state='running',worker_id='lost',lease_until='2020-01-01T00:00:00+00:00' WHERE id=?",(second,))
|
||||
assert jobs.recover_expired()==1
|
||||
assert db.one('SELECT state FROM build_jobs WHERE id=?',(second,))['state']=='retrying'
|
||||
|
||||
def test_real_subprocess_worker(web):
|
||||
c,sid=web
|
||||
jid=c.post(f'/api/sites/{sid}/builds',json={}).json()['id']
|
||||
worker.run_once() # real Python subprocess, reads same test SQLite database
|
||||
row=db.one('SELECT * FROM build_jobs WHERE id=?',(jid,))
|
||||
assert row['state']=='done',row['error']
|
||||
report=json.loads(row['result_json'])
|
||||
assert report['pages']>=1
|
||||
|
||||
def test_cancel_actively_running_subprocess(web,monkeypatch):
|
||||
import subprocess,sys,threading,time
|
||||
c,sid=web
|
||||
jobid=c.post(f'/api/sites/{sid}/builds',json={}).json()['id']
|
||||
original=subprocess.Popen
|
||||
monkeypatch.setattr(worker.subprocess,'Popen',lambda *args,**kwargs:original(
|
||||
[sys.executable,'-c','import time;time.sleep(10)'],**kwargs))
|
||||
t=threading.Thread(target=worker.run_once)
|
||||
t.start()
|
||||
for _ in range(100):
|
||||
row=db.one('SELECT state FROM build_jobs WHERE id=?',(jobid,))
|
||||
if row and row['state']=='running':break
|
||||
time.sleep(0.02)
|
||||
r=c.post(f'/api/enterprise/sites/{sid}/jobs/{jobid}/cancel')
|
||||
assert r.status_code==200
|
||||
t.join(timeout=6)
|
||||
assert not t.is_alive()
|
||||
assert db.one('SELECT state FROM build_jobs WHERE id=?',(jobid,))['state']=='cancelled'
|
||||
|
||||
|
||||
def test_build_timeout_sets_retrying(web,monkeypatch):
|
||||
import subprocess,sys
|
||||
c,sid=web
|
||||
jobid=c.post(f'/api/sites/{sid}/builds',json={}).json()['id']
|
||||
db.execute('UPDATE build_jobs SET timeout_seconds=1 WHERE id=?',(jobid,))
|
||||
original=subprocess.Popen
|
||||
monkeypatch.setattr(worker.subprocess,'Popen',lambda *args,**kwargs:original(
|
||||
[sys.executable,'-c','import time;time.sleep(10)'],**kwargs))
|
||||
worker.run_once()
|
||||
row=db.one('SELECT state,error FROM build_jobs WHERE id=?',(jobid,))
|
||||
assert row['state']=='retrying',row
|
||||
assert 'timed out' in row['error']
|
||||
Reference in New Issue
Block a user