From c48f8eef3343f5028af7903ddf0d9bca0d239626 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=A8=E6=88=B7CI=20Test?= Date: Sat, 11 Jul 2026 17:13:51 +0800 Subject: [PATCH 01/13] =?UTF-8?q?fix(voice-clone):=20source=5Faudio=5Furl?= =?UTF-8?q?=20=E4=B8=8D=E5=81=9A=E9=A2=84=E7=AD=BE=E5=90=8D=E8=BD=AC?= =?UTF-8?q?=E6=8D=A2=EF=BC=8C=E5=8E=9F=E6=A0=B7=E8=BF=94=E5=9B=9E=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E8=BE=93=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit source_audio_url 是用户传入的原始参考音频 URL(可能是外部地址), 不是系统内部 OSS storage key,不应该经过 storage.get_download_url 做预签名转换。之前的代码会把外部 URL 错误地当作 OSS key 处理, 导致返回无效的签名 URL。 - 删除 _to_response 中的 sign_url 调用和参数 - 清理 4 个端点中不再使用的 get_audio_url_signer 依赖注入 - 33 个 voice_clone 集成测试全绿 --- apps/api/app/api/routes/voice_clones.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) mode change 100755 => 100644 apps/api/app/api/routes/voice_clones.py diff --git a/apps/api/app/api/routes/voice_clones.py b/apps/api/app/api/routes/voice_clones.py old mode 100755 new mode 100644 index 740796283..ccbf28383 --- a/apps/api/app/api/routes/voice_clones.py +++ b/apps/api/app/api/routes/voice_clones.py @@ -6,7 +6,7 @@ import logging from typing import Optional from app.auth import AuthenticatedUser, get_current_user -from app.dependencies import get_audio_url_signer, get_cosyvoice_service, get_voice_clone_profile_repository +from app.dependencies import get_cosyvoice_service, get_voice_clone_profile_repository from app.schemas.voice_clone import ( CreateVoiceCloneRequest, ListVoiceCloneResponse, @@ -37,16 +37,14 @@ logger = logging.getLogger(__name__) router = APIRouter() -def _to_response(profile, sign_url=None) -> VoiceCloneProfileResponse: - source_url = profile.source_audio_url - if sign_url and source_url: - source_url = sign_url(source_url) +def _to_response(profile) -> VoiceCloneProfileResponse: + # source_audio_url 是用户传入的原始 URL(可能是外部地址),不做预签名转换 return VoiceCloneProfileResponse( id=profile.id, user_id=profile.user_id, name=profile.name, description=profile.description, - source_audio_url=source_url, + source_audio_url=profile.source_audio_url, voice_id=profile.voice_id, voice_model=profile.voice_model, language=profile.language, @@ -77,7 +75,6 @@ def create_voice_clone( request: CreateVoiceCloneRequest, authenticated_user: AuthenticatedUser = Depends(get_current_user), workflow: VoiceCloneWorkflowService = Depends(_get_workflow_service), - sign_url=Depends(get_audio_url_signer), ) -> VoiceCloneProfileResponse: """创建音色克隆任务。 @@ -113,7 +110,7 @@ def create_voice_clone( except Exception as inner_e: logger.error(f"Failed to mark profile as failed after dispatch error: {inner_e}") - return _to_response(profile, sign_url) + return _to_response(profile) @router.get("", response_model=ListVoiceCloneResponse) @@ -123,14 +120,13 @@ def list_voice_clones( limit: int = Query(50, ge=1, le=200), authenticated_user: AuthenticatedUser = Depends(get_current_user), repository: SQLAlchemyVoiceCloneProfileRepository = Depends(get_voice_clone_profile_repository), - sign_url=Depends(get_audio_url_signer), ) -> ListVoiceCloneResponse: """获取用户的音色克隆列表。""" user_id = authenticated_user.user.id use_case = ListVoiceClonesUseCase(repository) items, total = use_case.execute(user_id, status=status_filter, skip=skip, limit=limit) return ListVoiceCloneResponse( - items=[_to_response(p, sign_url) for p in items], + items=[_to_response(p) for p in items], total=total, ) @@ -140,7 +136,6 @@ def get_voice_clone( clone_id: str, authenticated_user: AuthenticatedUser = Depends(get_current_user), repository: SQLAlchemyVoiceCloneProfileRepository = Depends(get_voice_clone_profile_repository), - sign_url=Depends(get_audio_url_signer), ) -> VoiceCloneProfileResponse: """获取音色克隆详情。""" user_id = authenticated_user.user.id @@ -149,7 +144,7 @@ def get_voice_clone( profile = use_case.execute(clone_id, user_id) except VoiceCloneNotFoundError: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Voice clone not found") - return _to_response(profile, sign_url) + return _to_response(profile) @router.get("/{clone_id}/status", response_model=VoiceCloneStatusResponse) @@ -198,7 +193,6 @@ def retry_voice_clone( clone_id: str, authenticated_user: AuthenticatedUser = Depends(get_current_user), workflow: VoiceCloneWorkflowService = Depends(_get_workflow_service), - sign_url=Depends(get_audio_url_signer), ) -> VoiceCloneProfileResponse: """重试失败的音色克隆。 @@ -231,4 +225,4 @@ def retry_voice_clone( except Exception as inner_e: logger.error(f"Failed to mark profile as failed after dispatch error: {inner_e}") - return _to_response(profile, sign_url) + return _to_response(profile) -- 2.54.0 From da1fd29a22126562513e88ca40cfdaccfe5048d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=A8=E6=88=B7CI=20Test?= Date: Sat, 11 Jul 2026 18:24:35 +0800 Subject: [PATCH 02/13] =?UTF-8?q?style:=20=E4=BF=AE=E5=A4=8Dblack=20+=20is?= =?UTF-8?q?ort=E6=A0=BC=E5=BC=8F=E9=97=AE=E9=A2=98=EF=BC=88task=5Fenqueue.?= =?UTF-8?q?py=20+=20=E9=99=90=E6=B5=81=E6=B5=8B=E8=AF=95=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/app/core/task_enqueue.py | 16 +++++----------- tests/unit/test_task_queue_limit.py | 4 ++-- 2 files changed, 7 insertions(+), 13 deletions(-) mode change 100755 => 100644 tests/unit/test_task_queue_limit.py diff --git a/apps/api/app/core/task_enqueue.py b/apps/api/app/core/task_enqueue.py index 0b3048d65..a4c5f361b 100755 --- a/apps/api/app/core/task_enqueue.py +++ b/apps/api/app/core/task_enqueue.py @@ -6,8 +6,8 @@ from app.core.celery_app import celery_app logger = logging.getLogger(__name__) # ── 限流阈值常量(全系统统一管理,不要在业务代码里硬编码) ── -USER_PENDING_LIMIT = 3 # 单用户 pending 上限 -GLOBAL_PENDING_LIMIT = 20 # 全局 pending 上限 +USER_PENDING_LIMIT = 3 # 单用户 pending 上限 +GLOBAL_PENDING_LIMIT = 20 # 全局 pending 上限 class UserPendingLimitExceeded(Exception): @@ -71,9 +71,7 @@ def check_queue_limits( user_pending, user_pending_limit, ) - raise UserPendingLimitExceeded( - user_id=user_id, pending_count=user_pending, limit=user_pending_limit - ) + raise UserPendingLimitExceeded(user_id=user_id, pending_count=user_pending, limit=user_pending_limit) def _mark_task_failed_safely( @@ -157,9 +155,7 @@ def safe_enqueue_generation_task( user_pending, user_pending_limit, ) - exc = UserPendingLimitExceeded( - user_id=user_id, pending_count=user_pending, limit=user_pending_limit - ) + exc = UserPendingLimitExceeded(user_id=user_id, pending_count=user_pending, limit=user_pending_limit) _mark_task_failed_safely(task, generation_task_repository, log_prefix, str(exc)) raise exc @@ -201,9 +197,7 @@ def safe_enqueue_generation_task( exc: Exception = GlobalQueueFull(pending_count=global_after, limit=global_pending_limit) else: reason = f"用户 pending 超限(入队后): {user_after}/{user_pending_limit}" - exc = UserPendingLimitExceeded( - user_id=user_id, pending_count=user_after, limit=user_pending_limit - ) + exc = UserPendingLimitExceeded(user_id=user_id, pending_count=user_after, limit=user_pending_limit) logger.warning( "[队列限流] %s, task_id=%s, user_id=%s — 回滚状态为 failed", diff --git a/tests/unit/test_task_queue_limit.py b/tests/unit/test_task_queue_limit.py old mode 100755 new mode 100644 index 6a906df57..dc4e56b41 --- a/tests/unit/test_task_queue_limit.py +++ b/tests/unit/test_task_queue_limit.py @@ -1,8 +1,9 @@ """任务队列限流防护单元测试。""" + from __future__ import annotations -import sys import os +import sys from unittest.mock import MagicMock import pytest @@ -18,7 +19,6 @@ from app.core.task_enqueue import ( safe_enqueue_generation_task, ) - # --------------------------------------------------------------------------- # Mock helpers # --------------------------------------------------------------------------- -- 2.54.0 From db229bcc230d57cafacce66677f57cdfb8e69313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=A8=E6=88=B7CI=20Test?= Date: Sat, 11 Jul 2026 18:31:30 +0800 Subject: [PATCH 03/13] chore: trigger CI -- 2.54.0 From 8ed0ea21fec26bfe2ae56ec7bf26afa82b3c8257 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=A8=E6=88=B7CI=20Test?= Date: Sat, 11 Jul 2026 18:42:53 +0800 Subject: [PATCH 04/13] =?UTF-8?q?fix(ci):=20=E4=BF=AE=E5=A4=8Dci-cd.yml?= =?UTF-8?q?=E7=9A=84YAML=E8=AF=AD=E6=B3=95=E9=94=99=E8=AF=AF=EF=BC=8C?= =?UTF-8?q?=E5=B0=86Python=E8=A6=86=E7=9B=96=E7=8E=87=E6=B1=87=E6=80=BB?= =?UTF-8?q?=E5=92=8C=E5=A4=B1=E8=B4=A5=E9=80=9A=E7=9F=A5=E6=8A=BD=E5=88=B0?= =?UTF-8?q?=E7=8B=AC=E7=AB=8B=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/ci-cd.yml | 91 ++-------------------------------- scripts/ci_coverage_summary.py | 32 ++++++++++++ scripts/ci_notify_failure.py | 82 ++++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 88 deletions(-) create mode 100755 scripts/ci_coverage_summary.py create mode 100755 scripts/ci_notify_failure.py diff --git a/.gitea/workflows/ci-cd.yml b/.gitea/workflows/ci-cd.yml index 1ee5f0c20..1788bdde0 100755 --- a/.gitea/workflows/ci-cd.yml +++ b/.gitea/workflows/ci-cd.yml @@ -269,91 +269,14 @@ jobs: run: | set +e echo "=== 覆盖率汇总 ===" - if [ -f coverage.xml ]; then - python3 -c " -import xml.etree.ElementTree as ET -tree = ET.parse('coverage.xml') -root = tree.getroot() -line_rate = float(root.get('line-rate', 0)) * 100 -branch_rate = float(root.get('branch-rate', 0)) * 100 -lines_covered = int(root.get('lines-covered', 0)) -lines_valid = int(root.get('lines-valid', 0)) -print(f'行覆盖率: {line_rate:.2f}% ({lines_covered}/{lines_valid})') -print(f'分支覆盖率: {branch_rate:.2f}%') -print(f'门槛: 65%') -print(f'状态: {"PASS ✅" if line_rate >= 65 else "FAIL ❌"}') -" - else - echo "coverage.xml 不存在,跳过汇总" - fi - + python3 scripts/ci_coverage_summary.py - name: Notify CI failure if: failure() shell: sh run: | set +e echo "=== CI 失败通知 ===" - - # 收集失败信息 - FAILED_JOB="Validate Code Quality And Tests" - BRANCH="${GITHUB_REF_NAME:-unknown}" - COMMIT="${GITHUB_SHA:0:8}" - ACTOR="${GITHUB_ACTOR:-unknown}" - RUN_ID="${GITHUB_RUN_ID:-unknown}" - REPO="${GITHUB_REPOSITORY:-unknown}" - RUN_URL="https://git.xiaoxiajianji.com/${REPO}/actions/runs/${RUN_ID}" - - # 构造通知消息 - PAYLOAD=$(cat < /dev/null 2>&1 && echo "通知已发送" || echo "通知发送失败" - else - echo "未配置 CI_NOTIFY_WEBHOOK,跳过通知" - echo "如需启用,请在仓库 Settings -> Secrets and variables -> Actions 中添加 CI_NOTIFY_WEBHOOK" - fi + FAILED_JOB="Validate Code Quality And Tests" python3 scripts/ci_notify_failure.py - name: Build summary if: github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/main' @@ -364,15 +287,7 @@ print(f'状态: {"PASS ✅" if line_rate >= 65 else "FAIL ❌"}') echo "Branch: ${GITHUB_REF_NAME}" echo "Commit: ${GITHUB_SHA}" # 输出最终覆盖率 - if [ -f coverage.xml ]; then - python3 -c " -import xml.etree.ElementTree as ET -tree = ET.parse('coverage.xml') -root = tree.getroot() -line_rate = float(root.get('line-rate', 0)) * 100 -print(f'Total coverage: {line_rate:.2f}%') -" - fi + python3 scripts/ci_coverage_summary.py frontend-lint: name: Frontend Lint diff --git a/scripts/ci_coverage_summary.py b/scripts/ci_coverage_summary.py new file mode 100755 index 000000000..c66cf543b --- /dev/null +++ b/scripts/ci_coverage_summary.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +"""解析 coverage.xml 并输出覆盖率汇总。""" +import sys +import xml.etree.ElementTree as ET + +THRESHOLD = 65 # 行覆盖率门槛,百分比 + + +def main() -> int: + try: + tree = ET.parse("coverage.xml") + except FileNotFoundError: + print("coverage.xml 不存在,跳过汇总") + return 0 + + root = tree.getroot() + line_rate = float(root.get("line-rate", 0)) * 100 + branch_rate = float(root.get("branch-rate", 0)) * 100 + lines_covered = int(root.get("lines-covered", 0)) + lines_valid = int(root.get("lines-valid", 0)) + + print(f"行覆盖率: {line_rate:.2f}% ({lines_covered}/{lines_valid})") + print(f"分支覆盖率: {branch_rate:.2f}%") + print(f"门槛: {THRESHOLD}%") + status = "PASS ✅" if line_rate >= THRESHOLD else "FAIL ❌" + print(f"状态: {status}") + + return 0 if line_rate >= THRESHOLD else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/ci_notify_failure.py b/scripts/ci_notify_failure.py new file mode 100755 index 000000000..8e4faa74f --- /dev/null +++ b/scripts/ci_notify_failure.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""发送 CI 失败通知到飞书/项目群 webhook。""" +import json +import os +import sys +import urllib.request + + +def main() -> int: + webhook = os.environ.get("CI_NOTIFY_WEBHOOK", "") + if not webhook: + print("未配置 CI_NOTIFY_WEBHOOK,跳过通知") + print("如需启用,请在仓库 Settings -> Secrets and variables -> Actions 中添加 CI_NOTIFY_WEBHOOK") + return 0 + + failed_job = os.environ.get("FAILED_JOB", "Unknown Job") + branch = os.environ.get("GITHUB_REF_NAME", "unknown") + commit = os.environ.get("GITHUB_SHA", "unknown")[:8] + actor = os.environ.get("GITHUB_ACTOR", "unknown") + run_id = os.environ.get("GITHUB_RUN_ID", "unknown") + repo = os.environ.get("GITHUB_REPOSITORY", "unknown") + run_url = f"https://git.xiaoxiajianji.com/{repo}/actions/runs/{run_id}" + + payload = { + "msg_type": "interactive", + "card": { + "header": { + "title": { + "tag": "plain_text", + "content": "❌ CI 构建失败", + }, + "status": "red", + }, + "elements": [ + { + "tag": "div", + "text": { + "tag": "lark_md", + "content": ( + f"**任务**: {failed_job}\n" + f"**分支**: {branch}\n" + f"**提交**: {commit}\n" + f"**提交者**: {actor}\n" + f"**Run ID**: {run_id}" + ), + }, + }, + { + "tag": "action", + "actions": [ + { + "tag": "button", + "text": {"tag": "plain_text", "content": "查看失败日志"}, + "url": run_url, + "type": "danger", + } + ], + }, + ], + }, + } + + data = json.dumps(payload).encode("utf-8") + req = urllib.request.Request( + webhook, + data=data, + headers={"Content-Type": "application/json"}, + method="POST", + ) + try: + with urllib.request.urlopen(req, timeout=10) as resp: + resp.read() + print("通知已发送") + except Exception as e: + print(f"通知发送失败: {e}", file=sys.stderr) + return 1 + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) -- 2.54.0 From 8e81277eb80d8b4e05c3a1e8fa889919db885c28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=A8=E6=88=B7CI=20Test?= Date: Sat, 11 Jul 2026 18:48:23 +0800 Subject: [PATCH 05/13] =?UTF-8?q?style:=20=E4=BF=AE=E5=A4=8Dci=5Fnotify=5F?= =?UTF-8?q?failure.py=E7=9A=84black=E6=A0=BC=E5=BC=8F=E5=8C=96=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/ci_notify_failure.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/ci_notify_failure.py b/scripts/ci_notify_failure.py index 8e4faa74f..a8be3de03 100755 --- a/scripts/ci_notify_failure.py +++ b/scripts/ci_notify_failure.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 """发送 CI 失败通知到飞书/项目群 webhook。""" + import json import os import sys -- 2.54.0 From a353bbb1d2ce2e5d4486a7f033e569692fd084f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=A8=E6=88=B7CI=20Test?= Date: Sat, 11 Jul 2026 18:57:22 +0800 Subject: [PATCH 06/13] =?UTF-8?q?style:=20=E6=A0=BC=E5=BC=8F=E5=8C=96CI?= =?UTF-8?q?=E8=84=9A=E6=9C=AC=E6=96=87=E4=BB=B6=EF=BC=8C=E5=AF=B9=E9=BD=90?= =?UTF-8?q?black=E8=A7=84=E8=8C=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/ci_coverage_summary.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/ci_coverage_summary.py b/scripts/ci_coverage_summary.py index c66cf543b..24c2ae6a3 100755 --- a/scripts/ci_coverage_summary.py +++ b/scripts/ci_coverage_summary.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 """解析 coverage.xml 并输出覆盖率汇总。""" + import sys import xml.etree.ElementTree as ET -- 2.54.0 From 828d9aaa349834ddc0173b833609f8374165c09e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=A8=E6=88=B7CI=20Test?= Date: Sat, 11 Jul 2026 18:59:06 +0800 Subject: [PATCH 07/13] =?UTF-8?q?fix(ci):=20=E4=BF=AE=E5=A4=8D=E8=A6=86?= =?UTF-8?q?=E7=9B=96=E7=8E=87=E9=87=87=E9=9B=86=E9=97=AE=E9=A2=98=EF=BC=8C?= =?UTF-8?q?=E5=B0=86--cov=3Dapps=E6=94=B9=E4=B8=BA--cov=3Dapp=20--cov=3Dpa?= =?UTF-8?q?ckages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit apps 不是 Python 包(无 __init__.py),代码通过 PYTHONPATH=apps/api 导入为 app.*。CI 环境中 pytest-cov 无法匹配模块路径,导致覆盖率 0%。 改为使用实际可导入的包名 app 和 packages。 --- .gitea/workflows/ci-cd.yml | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) mode change 100755 => 100644 .gitea/workflows/ci-cd.yml diff --git a/.gitea/workflows/ci-cd.yml b/.gitea/workflows/ci-cd.yml old mode 100755 new mode 100644 index 1788bdde0..e9f98f6bc --- a/.gitea/workflows/ci-cd.yml +++ b/.gitea/workflows/ci-cd.yml @@ -165,7 +165,7 @@ jobs: run: | set -eu PYTHONPATH="$PWD/apps/api:$PWD" python3 -m pytest tests/unit -q \ - --cov=apps --cov-report=term --cov-report=term-missing --cov-report=xml \ + --cov=app --cov=packages --cov-report=term --cov-report=term-missing --cov-report=xml \ --cov-fail-under=60 - name: Start PostgreSQL for integration tests @@ -212,7 +212,7 @@ jobs: set -eu pip install -q pytest-rerunfailures PYTHONPATH="$PWD/apps/api:$PWD" python3 -m pytest tests/integration -q --timeout=60 -x --reruns 2 --reruns-delay 1 -m "not performance" \ - --cov=apps --cov-append --cov-report=term --cov-report=term-missing --cov-report=xml --cov-fail-under=65 + --cov=app --cov=packages --cov-append --cov-report=term --cov-report=term-missing --cov-report=xml --cov-fail-under=65 - name: Run API performance baseline tests shell: sh diff --git a/pyproject.toml b/pyproject.toml index 2d06cc424..d34b6e775 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ extend_skip_glob = [ ] [tool.coverage.run] -source = ["apps", "packages"] +source = ["app", "packages"] omit = [ "*/migrations/*", "*/tests/*", -- 2.54.0 From 0ffa0f5c44685addacc4aad93555d6edd89e18df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=A8=E6=88=B7CI=20Test?= Date: Sat, 11 Jul 2026 19:03:25 +0800 Subject: [PATCH 08/13] =?UTF-8?q?fix(ci):=20coverage=E6=94=B9=E7=94=A8?= =?UTF-8?q?=E6=98=BE=E5=BC=8F=E7=9B=AE=E5=BD=95=E8=B7=AF=E5=BE=84=EF=BC=8C?= =?UTF-8?q?=E9=81=BF=E5=85=8D=E6=A8=A1=E5=9D=97=E5=90=8D=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI环境中pytest-cov无法通过模块名(app)找到源码目录,导致覆盖率0%。 改用显式目录路径 apps/api/app 直接追踪文件。 --- .gitea/workflows/ci-cd.yml | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/ci-cd.yml b/.gitea/workflows/ci-cd.yml index e9f98f6bc..200e0c5a1 100644 --- a/.gitea/workflows/ci-cd.yml +++ b/.gitea/workflows/ci-cd.yml @@ -165,7 +165,7 @@ jobs: run: | set -eu PYTHONPATH="$PWD/apps/api:$PWD" python3 -m pytest tests/unit -q \ - --cov=app --cov=packages --cov-report=term --cov-report=term-missing --cov-report=xml \ + --cov=apps/api/app --cov=packages --cov-report=term --cov-report=term-missing --cov-report=xml \ --cov-fail-under=60 - name: Start PostgreSQL for integration tests @@ -212,7 +212,7 @@ jobs: set -eu pip install -q pytest-rerunfailures PYTHONPATH="$PWD/apps/api:$PWD" python3 -m pytest tests/integration -q --timeout=60 -x --reruns 2 --reruns-delay 1 -m "not performance" \ - --cov=app --cov=packages --cov-append --cov-report=term --cov-report=term-missing --cov-report=xml --cov-fail-under=65 + --cov=apps/api/app --cov=packages --cov-append --cov-report=term --cov-report=term-missing --cov-report=xml --cov-fail-under=65 - name: Run API performance baseline tests shell: sh diff --git a/pyproject.toml b/pyproject.toml index d34b6e775..1f6b4118c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ extend_skip_glob = [ ] [tool.coverage.run] -source = ["app", "packages"] +source = ["apps/api/app", "packages"] omit = [ "*/migrations/*", "*/tests/*", -- 2.54.0 From 9581773b7e29ae72da521ad5be5a7968ec4c470b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=A8=E6=88=B7CI=20Test?= Date: Sat, 11 Jul 2026 19:06:58 +0800 Subject: [PATCH 09/13] =?UTF-8?q?fix(ci):=20=E6=94=B9=E7=94=A8coverage=20r?= =?UTF-8?q?un=E6=9B=BF=E4=BB=A3pytest-cov=EF=BC=8C=E8=A7=A3=E5=86=B3CI?= =?UTF-8?q?=E8=A6=86=E7=9B=96=E7=8E=870%=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pytest-cov的--cov在CI环境中无法正确追踪源码(No data was collected), 改用 coverage run 直接调用,确保源码路径正确匹配。 --- .gitea/workflows/ci-cd.yml | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) mode change 100644 => 100755 .gitea/workflows/ci-cd.yml diff --git a/.gitea/workflows/ci-cd.yml b/.gitea/workflows/ci-cd.yml old mode 100644 new mode 100755 index 200e0c5a1..899ebe833 --- a/.gitea/workflows/ci-cd.yml +++ b/.gitea/workflows/ci-cd.yml @@ -164,9 +164,14 @@ jobs: USE_IN_MEMORY_DB: "true" run: | set -eu - PYTHONPATH="$PWD/apps/api:$PWD" python3 -m pytest tests/unit -q \ - --cov=apps/api/app --cov=packages --cov-report=term --cov-report=term-missing --cov-report=xml \ - --cov-fail-under=60 + PYTHONPATH="$PWD/apps/api:$PWD" python3 -m coverage run \ + --source=apps/api/app,packages \ + --omit="*/migrations/*,*/tests/*,*/test_*.py,*/site-packages/*,*/.cache/*" \ + --branch \ + -m pytest tests/unit -q + python3 -m coverage report --show-missing + python3 -m coverage xml -o coverage.xml + python3 -m coverage report --fail-under=60 > /dev/null - name: Start PostgreSQL for integration tests shell: sh @@ -211,8 +216,14 @@ jobs: run: | set -eu pip install -q pytest-rerunfailures - PYTHONPATH="$PWD/apps/api:$PWD" python3 -m pytest tests/integration -q --timeout=60 -x --reruns 2 --reruns-delay 1 -m "not performance" \ - --cov=apps/api/app --cov=packages --cov-append --cov-report=term --cov-report=term-missing --cov-report=xml --cov-fail-under=65 + PYTHONPATH="$PWD/apps/api:$PWD" python3 -m coverage run --append \ + --source=apps/api/app,packages \ + --omit="*/migrations/*,*/tests/*,*/test_*.py,*/site-packages/*,*/.cache/*" \ + --branch \ + -m pytest tests/integration -q --timeout=60 -x --reruns 2 --reruns-delay 1 -m "not performance" + python3 -m coverage report --show-missing + python3 -m coverage xml -o coverage.xml + python3 -m coverage report --fail-under=65 > /dev/null - name: Run API performance baseline tests shell: sh -- 2.54.0 From b32a1a5de3bfbdbae0060b2c9ba060e8bf75e898 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=A8=E6=88=B7CI=20Test?= Date: Sat, 11 Jul 2026 19:10:39 +0800 Subject: [PATCH 10/13] =?UTF-8?q?debug:=20=E6=B7=BB=E5=8A=A0=E8=A6=86?= =?UTF-8?q?=E7=9B=96=E7=8E=87=E8=B7=AF=E5=BE=84=E8=B0=83=E8=AF=95=E6=AD=A5?= =?UTF-8?q?=E9=AA=A4=EF=BC=88=E4=BF=AE=E6=AD=A3YAML=E7=BC=A9=E8=BF=9B?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/ci-cd.yml | 47 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/.gitea/workflows/ci-cd.yml b/.gitea/workflows/ci-cd.yml index 899ebe833..07a0acde7 100755 --- a/.gitea/workflows/ci-cd.yml +++ b/.gitea/workflows/ci-cd.yml @@ -158,6 +158,53 @@ jobs: python3 scripts/check_migration_safety.py --allow-medium-risk fi + - name: Debug coverage paths + shell: sh + run: | + set +e + echo "=== PWD ===" + pwd + echo "=== check source dirs ===" + ls -d apps/api/app packages + echo "=== python import check ===" + python3 - <<'PY' + import sys, os + os.environ["PYTHONPATH"] = f"{os.getcwd()}/apps/api:{os.getcwd()}" + sys.path.insert(0, f"{os.getcwd()}/apps/api") + sys.path.insert(0, os.getcwd()) + print(f"cwd: {os.getcwd()}") + print(f"sys.path[:5]: {sys.path[:5]}") + try: + import app + print(f"app.__file__: {app.__file__}") + except Exception as e: + print(f"import app failed: {e}") + try: + import packages + print(f"packages.__file__: {packages.__file__}") + except Exception as e: + print(f"import packages failed: {e}") + PY + echo "=== coverage debug ===" + python3 - <<'PY' + import os, sys + sys.path.insert(0, f"{os.getcwd()}/apps/api") + sys.path.insert(0, os.getcwd()) + import coverage + cov = coverage.Coverage(source=["apps/api/app", "packages"]) + print(f"source: {cov.config.source}") + for src in cov.config.source or []: + abspath = os.path.abspath(src) + print(f" {src} -> {abspath} exists={os.path.exists(src)}") + if os.path.isdir(src): + pyfiles = [] + for root, dirs, files in os.walk(src): + for f in files: + if f.endswith('.py'): + pyfiles.append(os.path.join(root, f)) + print(f" .py files: {len(pyfiles)}") + PY + - name: Run unit tests shell: sh env: -- 2.54.0 From ebf4a1e3841783ea40c92252a23aad954f5da675 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=A8=E6=88=B7CI=20Test?= Date: Sat, 11 Jul 2026 19:13:04 +0800 Subject: [PATCH 11/13] =?UTF-8?q?fix(ci):=20=E7=A7=BB=E9=99=A4=E8=A6=86?= =?UTF-8?q?=E7=9B=96=E7=8E=87omit=E4=B8=AD=E7=9A=84*/.cache/*=E8=A7=84?= =?UTF-8?q?=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI runner的工作目录在.cache/act/...下,*/.cache/*的omit规则 导致所有源码文件都被排除,覆盖率为0%。移除此规则。 --- .gitea/workflows/ci-cd.yml | 4 ++-- pyproject.toml | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) mode change 100644 => 100755 pyproject.toml diff --git a/.gitea/workflows/ci-cd.yml b/.gitea/workflows/ci-cd.yml index 07a0acde7..a689a30d8 100755 --- a/.gitea/workflows/ci-cd.yml +++ b/.gitea/workflows/ci-cd.yml @@ -213,7 +213,7 @@ jobs: set -eu PYTHONPATH="$PWD/apps/api:$PWD" python3 -m coverage run \ --source=apps/api/app,packages \ - --omit="*/migrations/*,*/tests/*,*/test_*.py,*/site-packages/*,*/.cache/*" \ + --omit="*/migrations/*,*/tests/*,*/test_*.py,*/site-packages/*" \ --branch \ -m pytest tests/unit -q python3 -m coverage report --show-missing @@ -265,7 +265,7 @@ jobs: pip install -q pytest-rerunfailures PYTHONPATH="$PWD/apps/api:$PWD" python3 -m coverage run --append \ --source=apps/api/app,packages \ - --omit="*/migrations/*,*/tests/*,*/test_*.py,*/site-packages/*,*/.cache/*" \ + --omit="*/migrations/*,*/tests/*,*/test_*.py,*/site-packages/*" \ --branch \ -m pytest tests/integration -q --timeout=60 -x --reruns 2 --reruns-delay 1 -m "not performance" python3 -m coverage report --show-missing diff --git a/pyproject.toml b/pyproject.toml old mode 100644 new mode 100755 index 1f6b4118c..7a75f45dd --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,6 @@ omit = [ "*/tests/*", "*/test_*.py", "*/site-packages/*", - "*/.cache/*", ] branch = true -- 2.54.0 From 6cd9ed351d66f6d1aacce63de6240ccd0bc67544 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=A8=E6=88=B7CI=20Test?= Date: Sat, 11 Jul 2026 19:17:38 +0800 Subject: [PATCH 12/13] =?UTF-8?q?fix(test):=20=E8=A1=A5=E5=85=A8StubGenera?= =?UTF-8?q?tionTaskRepository=E7=9A=84=E9=99=90=E6=B5=81=E6=96=B9=E6=B3=95?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E5=A4=8D=E9=9B=86=E6=88=90=E6=B5=8B=E8=AF=95?= =?UTF-8?q?AttributeError?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/integration/test_generation_api.py | 11 +++++++++++ tests/integration/test_task_center_api.py | 11 +++++++++++ tests/unit/test_edit_plan_generation_api.py | 9 +++++++++ tests/unit/test_edit_plan_service.py | 6 ++++++ tests/unit/test_generation_presigned_url.py | 6 ++++++ 5 files changed, 43 insertions(+) mode change 100644 => 100755 tests/integration/test_generation_api.py mode change 100644 => 100755 tests/integration/test_task_center_api.py mode change 100644 => 100755 tests/unit/test_edit_plan_generation_api.py mode change 100644 => 100755 tests/unit/test_edit_plan_service.py mode change 100644 => 100755 tests/unit/test_generation_presigned_url.py diff --git a/tests/integration/test_generation_api.py b/tests/integration/test_generation_api.py old mode 100644 new mode 100755 index 867985de4..7a179f4ac --- a/tests/integration/test_generation_api.py +++ b/tests/integration/test_generation_api.py @@ -118,6 +118,17 @@ class StubGenerationTaskRepository: def count_by_user(self, user_id: str) -> int: return len([t for t in self._tasks.values() if t.created_by_user_id == user_id]) + def count_pending_by_user(self, user_id: str) -> int: + from packages.domain.generation_task import GenerationTaskStatus + return len([ + t for t in self._tasks.values() + if t.created_by_user_id == user_id and t.status == GenerationTaskStatus.PENDING + ]) + + def count_pending_total(self) -> int: + from packages.domain.generation_task import GenerationTaskStatus + return len([t for t in self._tasks.values() if t.status == GenerationTaskStatus.PENDING]) + def list_recent_by_user(self, user_id: str, limit: int = 5) -> list[GenerationTask]: items = [t for t in self._tasks.values() if t.created_by_user_id == user_id] items.sort(key=lambda t: t.created_at, reverse=True) diff --git a/tests/integration/test_task_center_api.py b/tests/integration/test_task_center_api.py old mode 100644 new mode 100755 index ee491a64c..0244a4521 --- a/tests/integration/test_task_center_api.py +++ b/tests/integration/test_task_center_api.py @@ -83,6 +83,17 @@ class StubGenerationTaskRepository: def count_by_user(self, user_id: str) -> int: return len([t for t in self._tasks.values() if t.created_by_user_id == user_id]) + def count_pending_by_user(self, user_id: str) -> int: + from packages.domain.generation_task import GenerationTaskStatus + return len([ + t for t in self._tasks.values() + if t.created_by_user_id == user_id and t.status == GenerationTaskStatus.PENDING + ]) + + def count_pending_total(self) -> int: + from packages.domain.generation_task import GenerationTaskStatus + return len([t for t in self._tasks.values() if t.status == GenerationTaskStatus.PENDING]) + def list_recent_by_user(self, user_id: str, limit: int = 5) -> list[GenerationTask]: items = [t for t in self._tasks.values() if t.created_by_user_id == user_id] items.sort(key=lambda t: t.created_at, reverse=True) diff --git a/tests/unit/test_edit_plan_generation_api.py b/tests/unit/test_edit_plan_generation_api.py old mode 100644 new mode 100755 index 37b8601d4..074289b40 --- a/tests/unit/test_edit_plan_generation_api.py +++ b/tests/unit/test_edit_plan_generation_api.py @@ -177,6 +177,15 @@ class StubGenerationTaskRepository: def count_by_user(self, user_id: str) -> int: return len([t for t in self._store.values() if t.created_by_user_id == user_id]) + def count_pending_by_user(self, user_id: str) -> int: + return len([ + t for t in self._store.values() + if t.created_by_user_id == user_id and getattr(t, "status", "") == "pending" + ]) + + def count_pending_total(self) -> int: + return len([t for t in self._store.values() if getattr(t, "status", "") == "pending"]) + def list_recent_by_user(self, user_id: str, limit: int = 5) -> list[Any]: items = [t for t in self._store.values() if t.created_by_user_id == user_id] items.sort(key=lambda t: t.created_at, reverse=True) diff --git a/tests/unit/test_edit_plan_service.py b/tests/unit/test_edit_plan_service.py old mode 100644 new mode 100755 index 9388c8a40..eb9be6c2c --- a/tests/unit/test_edit_plan_service.py +++ b/tests/unit/test_edit_plan_service.py @@ -194,6 +194,12 @@ class StubGenerationTaskRepository: self._tasks[task.id] = task return task + def count_pending_by_user(self, user_id: str) -> int: + return 0 + + def count_pending_total(self) -> int: + return 0 + # --------------------------------------------------------------------------- # Service factory diff --git a/tests/unit/test_generation_presigned_url.py b/tests/unit/test_generation_presigned_url.py old mode 100644 new mode 100755 index 12caf3ce7..8a93983d9 --- a/tests/unit/test_generation_presigned_url.py +++ b/tests/unit/test_generation_presigned_url.py @@ -58,6 +58,12 @@ class StubGenerationTaskRepository: def get(self, task_id): return self._tasks.get(task_id) + def count_pending_by_user(self, user_id): + return 0 + + def count_pending_total(self): + return 0 + class StubGeneratedVideoRepository: def __init__(self, videos=None): -- 2.54.0 From 1d773d44c8f0a4a16996d88b2c55cb9cbc446e43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=A8=E6=88=B7CI=20Test?= Date: Sat, 11 Jul 2026 19:24:53 +0800 Subject: [PATCH 13/13] =?UTF-8?q?style:=20=E4=BF=AE=E5=A4=8D=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E6=A1=A9=E4=BB=A3=E7=A0=81=E6=A0=BC=E5=BC=8F=EF=BC=8C?= =?UTF-8?q?=E5=AF=B9=E9=BD=90black/isort/flake8=E8=A7=84=E8=8C=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/integration/test_generation_api.py | 13 +++++++------ tests/integration/test_task_center_api.py | 13 +++++++------ tests/unit/test_edit_plan_generation_api.py | 11 +++++++---- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/tests/integration/test_generation_api.py b/tests/integration/test_generation_api.py index 7a179f4ac..5a609d7af 100755 --- a/tests/integration/test_generation_api.py +++ b/tests/integration/test_generation_api.py @@ -119,14 +119,15 @@ class StubGenerationTaskRepository: return len([t for t in self._tasks.values() if t.created_by_user_id == user_id]) def count_pending_by_user(self, user_id: str) -> int: - from packages.domain.generation_task import GenerationTaskStatus - return len([ - t for t in self._tasks.values() - if t.created_by_user_id == user_id and t.status == GenerationTaskStatus.PENDING - ]) + return len( + [ + t + for t in self._tasks.values() + if t.created_by_user_id == user_id and t.status == GenerationTaskStatus.PENDING + ] + ) def count_pending_total(self) -> int: - from packages.domain.generation_task import GenerationTaskStatus return len([t for t in self._tasks.values() if t.status == GenerationTaskStatus.PENDING]) def list_recent_by_user(self, user_id: str, limit: int = 5) -> list[GenerationTask]: diff --git a/tests/integration/test_task_center_api.py b/tests/integration/test_task_center_api.py index 0244a4521..bec726186 100755 --- a/tests/integration/test_task_center_api.py +++ b/tests/integration/test_task_center_api.py @@ -84,14 +84,15 @@ class StubGenerationTaskRepository: return len([t for t in self._tasks.values() if t.created_by_user_id == user_id]) def count_pending_by_user(self, user_id: str) -> int: - from packages.domain.generation_task import GenerationTaskStatus - return len([ - t for t in self._tasks.values() - if t.created_by_user_id == user_id and t.status == GenerationTaskStatus.PENDING - ]) + return len( + [ + t + for t in self._tasks.values() + if t.created_by_user_id == user_id and t.status == GenerationTaskStatus.PENDING + ] + ) def count_pending_total(self) -> int: - from packages.domain.generation_task import GenerationTaskStatus return len([t for t in self._tasks.values() if t.status == GenerationTaskStatus.PENDING]) def list_recent_by_user(self, user_id: str, limit: int = 5) -> list[GenerationTask]: diff --git a/tests/unit/test_edit_plan_generation_api.py b/tests/unit/test_edit_plan_generation_api.py index 074289b40..b7ee3c545 100755 --- a/tests/unit/test_edit_plan_generation_api.py +++ b/tests/unit/test_edit_plan_generation_api.py @@ -178,10 +178,13 @@ class StubGenerationTaskRepository: return len([t for t in self._store.values() if t.created_by_user_id == user_id]) def count_pending_by_user(self, user_id: str) -> int: - return len([ - t for t in self._store.values() - if t.created_by_user_id == user_id and getattr(t, "status", "") == "pending" - ]) + return len( + [ + t + for t in self._store.values() + if t.created_by_user_id == user_id and getattr(t, "status", "") == "pending" + ] + ) def count_pending_total(self) -> int: return len([t for t in self._store.values() if getattr(t, "status", "") == "pending"]) -- 2.54.0