53fb25efcf
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Check push changed paths (push) Successful in 19s
CI/CD Pipeline / Build Staging API Image (push) Successful in 41s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 48s
CI/CD Pipeline / Integration Tests (push) Successful in 3m10s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 3m17s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m30s
CI/CD Pipeline / Validate - Style (push) Successful in 4m17s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 59s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 5m33s
CI/CD Pipeline / Validate - Security (push) Successful in 7m12s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m38s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m13s
CI/CD Pipeline / Unit Tests (push) Successful in 10m11s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Failing after 26h14m3s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 26h24m21s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 26h19m47s
CI/CD Pipeline / PR Build Web Image (push) Failing after 26h23m44s
CI/CD Pipeline / PR Build API Image (push) Failing after 26h23m44s
CI/CD Pipeline / Deploy Production (push) Failing after 26h13m23s
CI/CD Pipeline / Build Production Web Image (push) Failing after 26h13m26s
CI/CD Pipeline / CI Gate (push) Failing after 26h13m25s
CI/CD Pipeline / Build Production API Image (push) Failing after 26h13m26s
CI/CD Pipeline / Canary Release to Production (push) Failing after 26h13m23s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 26h19m46s
CI/CD Pipeline / Frontend Lint (push) Failing after 26h23m37s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 26h23m45s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 26h19m46s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
152 lines
4.9 KiB
Python
152 lines
4.9 KiB
Python
"""GenerationTaskRepository - cleanup_stale_pending 超时 pending 清理单元测试。"""
|
|
|
|
import sys
|
|
from datetime import UTC, datetime, timedelta, timezone
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api"))
|
|
|
|
from sqlalchemy import create_engine, text
|
|
from sqlalchemy.orm import sessionmaker
|
|
|
|
from packages.adapters.sqlalchemy_impl.generation_task_repository import (
|
|
SQLAlchemyGenerationTaskRepository,
|
|
)
|
|
from packages.adapters.sqlalchemy_impl.models import Base
|
|
from packages.domain import GenerationTask, GenerationTaskStatus
|
|
|
|
|
|
def _repository():
|
|
engine = create_engine("sqlite:///:memory:")
|
|
Base.metadata.create_all(engine)
|
|
session = sessionmaker(bind=engine)()
|
|
return SQLAlchemyGenerationTaskRepository(session), session, engine
|
|
|
|
|
|
def _make_task(**kwargs) -> GenerationTask:
|
|
defaults = dict(
|
|
project_id="proj-1",
|
|
asset_library_id="lib-1",
|
|
created_by_user_id="user-1",
|
|
)
|
|
defaults.update(kwargs)
|
|
return GenerationTask.create(**defaults)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# cleanup_stale_pending 基本测试
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_cleanup_stale_pending_no_tasks_returns_zero():
|
|
"""没有任务时返回 0。"""
|
|
repo, _, _ = _repository()
|
|
count = repo.cleanup_stale_pending(timeout_minutes=30)
|
|
assert count == 0
|
|
|
|
|
|
def test_cleanup_stale_pending_recent_pending_not_cleaned():
|
|
"""30 分钟内的 pending 任务不被清理。"""
|
|
repo, _, _ = _repository()
|
|
task = _make_task()
|
|
repo.create(task)
|
|
# 刚创建的 pending 任务不应被清理
|
|
count = repo.cleanup_stale_pending(timeout_minutes=30)
|
|
assert count == 0
|
|
assert repo.get(task.id).status == GenerationTaskStatus.PENDING
|
|
|
|
|
|
def test_cleanup_stale_pending_old_pending_marked_failed():
|
|
"""超过 30 分钟的 pending 任务被标记为 failed。"""
|
|
repo, _, engine = _repository()
|
|
task = _make_task()
|
|
repo.create(task)
|
|
|
|
# 手动把 created_at 改到 1 小时前
|
|
with engine.connect() as conn:
|
|
conn.execute(
|
|
text("UPDATE generation_tasks SET created_at = :ts WHERE id = :id"),
|
|
{"ts": datetime.now(UTC) - timedelta(hours=1), "id": task.id},
|
|
)
|
|
conn.commit()
|
|
|
|
count = repo.cleanup_stale_pending(timeout_minutes=30)
|
|
assert count == 1
|
|
|
|
saved = repo.get(task.id)
|
|
assert saved.status == GenerationTaskStatus.FAILED
|
|
assert saved.error_message == "pending timeout: auto cleanup"
|
|
assert saved.error_info.get("error_type") == "PendingTimeout"
|
|
assert "30" in saved.error_info["message"]
|
|
assert "failed_at" in saved.error_info
|
|
assert saved.completed_at is not None
|
|
|
|
|
|
def test_cleanup_stale_pending_running_not_touched():
|
|
"""running 任务不受影响,只清理 pending。"""
|
|
repo, _, engine = _repository()
|
|
task = _make_task()
|
|
repo.create(task)
|
|
task.mark_processing()
|
|
repo.update(task)
|
|
|
|
# 回写 created_at 到 1 小时前
|
|
with engine.connect() as conn:
|
|
conn.execute(
|
|
text("UPDATE generation_tasks SET created_at = :ts WHERE id = :id"),
|
|
{"ts": datetime.now(UTC) - timedelta(hours=1), "id": task.id},
|
|
)
|
|
conn.commit()
|
|
|
|
count = repo.cleanup_stale_pending(timeout_minutes=30)
|
|
assert count == 0
|
|
assert repo.get(task.id).status == GenerationTaskStatus.RUNNING
|
|
|
|
|
|
def test_cleanup_stale_pending_custom_timeout():
|
|
"""自定义超时时间生效。"""
|
|
repo, _, engine = _repository()
|
|
task = _make_task()
|
|
repo.create(task)
|
|
|
|
# 回写 created_at 到 20 分钟前
|
|
with engine.connect() as conn:
|
|
conn.execute(
|
|
text("UPDATE generation_tasks SET created_at = :ts WHERE id = :id"),
|
|
{"ts": datetime.now(UTC) - timedelta(minutes=20), "id": task.id},
|
|
)
|
|
conn.commit()
|
|
|
|
# 30 分钟超时:不清理
|
|
count_30 = repo.cleanup_stale_pending(timeout_minutes=30)
|
|
assert count_30 == 0
|
|
# 15 分钟超时:清理
|
|
count_15 = repo.cleanup_stale_pending(timeout_minutes=15)
|
|
assert count_15 == 1
|
|
assert repo.get(task.id).status == GenerationTaskStatus.FAILED
|
|
|
|
|
|
def test_cleanup_stale_pending_multiple():
|
|
"""批量清理多个超时的 pending 任务。"""
|
|
repo, _, engine = _repository()
|
|
|
|
tasks = []
|
|
for i in range(5):
|
|
t = _make_task(project_id=f"proj-{i}")
|
|
repo.create(t)
|
|
tasks.append(t)
|
|
|
|
# 全部回写 created_at 到 2 小时前
|
|
with engine.connect() as conn:
|
|
for t in tasks:
|
|
conn.execute(
|
|
text("UPDATE generation_tasks SET created_at = :ts WHERE id = :id"),
|
|
{"ts": datetime.now(UTC) - timedelta(hours=2), "id": t.id},
|
|
)
|
|
conn.commit()
|
|
|
|
count = repo.cleanup_stale_pending(timeout_minutes=30)
|
|
assert count == 5
|
|
for t in tasks:
|
|
assert repo.get(t.id).status == GenerationTaskStatus.FAILED
|