Files
cms-SiteForge/tests/test_worker_queue_v6.py
浅念 8cea7dee20 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>
2026-10-22 16:49:25 +08:00

110 lines
5.3 KiB
Python

"""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']