From be2e16bf47cd97aeeb0dddfc137b280b4d519d0f Mon Sep 17 00:00:00 2001 From: Curry Date: Sat, 18 Jul 2026 19:10:17 +0800 Subject: [PATCH] fix(chat): surface trigger_task dispatch failure instead of applied=true MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /chat/confirm trigger_task: wrap dispatch_collection in try/except; dispatch failure after task commit now returns 502 with task_id instead of swallowing the error and reporting applied=true (F841 result unused) - success response now includes executor dispatch info - tests/conftest.py: clear API_AUTH_TOKEN/AGENT_API_TOKEN before backend import — deployment .env with fleet token was failing 19 unit tests (401) - add dispatch-failure regression test Triage ledger: docs/BUG-TRIAGE-20260718.md --- backend/api/v1/chat.py | 20 +++++++++-- docs/BUG-TRIAGE-20260718.md | 53 ++++++++++++++++++++++++++++++ tests/conftest.py | 7 ++++ tests/integration/test_chat_api.py | 39 ++++++++++++++++++++++ 4 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 docs/BUG-TRIAGE-20260718.md diff --git a/backend/api/v1/chat.py b/backend/api/v1/chat.py index 12b52eb..4c22ee1 100644 --- a/backend/api/v1/chat.py +++ b/backend/api/v1/chat.py @@ -434,9 +434,25 @@ async def confirm(body: ConfirmRequest, db: AsyncSession = Depends(get_db)) -> A await db.commit() from backend.executor import get_executor - result = await get_executor().dispatch_collection(task.id, {}) + try: + dispatch = await get_executor().dispatch_collection(task.id, {}) + except Exception as exc: + # Task row is already committed; surface the dispatch failure instead + # of reporting applied=True with a silently dead task. + logger.exception("chat confirm | trigger_task dispatch failed source=%s task=%s", source.id, task.id) + raise HTTPException( + status_code=502, detail=f"任务已创建但派发失败 (task_id={task.id}), 请到工作项里重试" + ) from exc logger.info("chat confirm | trigger_task source=%s task=%s", source.id, task.id) - return ApiResponse.ok({"applied": True, "tool": proposal.tool, "task_id": task.id, "summary": proposal.summary}) + return ApiResponse.ok( + { + "applied": True, + "tool": proposal.tool, + "task_id": task.id, + "summary": proposal.summary, + "dispatch": dispatch, + } + ) if proposal.tool == "update_schedule": schedule = await schedule_service.get_schedule(db, args.get("schedule_id", "")) diff --git a/docs/BUG-TRIAGE-20260718.md b/docs/BUG-TRIAGE-20260718.md new file mode 100644 index 0000000..0372c87 --- /dev/null +++ b/docs/BUG-TRIAGE-20260718.md @@ -0,0 +1,53 @@ +# Backend Bug Triage — 2026-07-18 + +部署验证 + 测试全量跑完后的后端问题账本。环境: main @ 50f48c8, Docker api+agent-1 (源码构建 0.3.6), host uv 环境跑测试。 + +## P0 — 结构性 + +### 1. identity 后端未进 main, production 前端登录死路 +- 前端 `app/login/page.tsx` 三通道: OIDC (未配置) / bootstrap (`signInWithBootstrap` → `GET /api/v1/auth/me`) / 本地开发模式 (仅 `NODE_ENV !== 'production'`) +- main 后端 `backend/security/` 只有 `fleet_auth.py` + `url_guard.py`; `/auth/me`、`BOOTSTRAP_ADMIN_TOKEN` 的 identity 模块只存在于 `origin/codex/notification-ack`、`origin/codex/workflow-studio-motion-wip` +- 后果: `next build` 产物在 main 上**无法登录任何账号**。当前部署被迫用 `next dev` (工程模式) 绕过 +- 修法: 把 codex 分支 identity 模块 (backend/security/identity.py + /auth/me 路由) 合回 main, 或前端登录页在 identity 后端缺席时降级 + +## P1 — 测试红 (真回归) + +### 2. workflow import / demand-draft / turbopush 与 plan-IR 校验器脱节 (integration 9 挂) +- `tests/integration/test_workflow_patch_api.py` ×8 + `test_workflow_turbopush_publish_api.py` ×1 +- 复现: `POST /api/v1/workflows/import/external-runtime` (langgraph 图) 返回 `valid: false` +- 根因错误码: + - `plan_ir_orphan_merge` — importer 把 langgraph `merge` 映射为 `intelligence.flow.merge` (要求 ≥2 入边), 导入图只有 1 入边 + - `plan_ir_port_type_mismatch` — `external.tool.capability` 出口 `type='unknown'` 接不上 merge 入口 `recordCandidate[]` +- plan_ir 校验在 c42ece1 (four-level node hierarchy) 接入 compiler; importer (`backend/workflow/external_importer.py`) 没跟着更新映射 +- 修向 (二选一): importer 合成合法图 (merge 补占位入边 / 外部工具出口给宽松类型), 或 plan-IR 对 external.* 目录节点放宽端口类型。牵涉工作流语义, 建议和工作流重构讨论一起定 + +## P2 + +### 3. chat confirm `trigger_task` 吞掉派发失败 +- `backend/api/v1/chat.py:437`: `result = await get_executor().dispatch_collection(task.id, {})` 结果未检查, 派发炸了 API 仍回 `applied: true` +- 修法: 检查 result / try-except 回 `applied: false` + 原因 + +## P3 + +### 4. 单测无 .env 隔离 +- 根 `.env` 配了 `API_AUTH_TOKEN` (部署必需) 时, `tests/unit` 挂 19 个 (workers/nodes_install/geo 全是 401 或 token 注入断言) +- pydantic Settings 直读仓库根 `.env`; conftest 未清空鉴权相关 env +- 修法: conftest autouse fixture 强制 `API_AUTH_TOKEN=''` + +### 5. 杂项 +- B904 raise-without-from 集中在 `backend/api/v1/browsers.py` (~8 处), 异常链丢失 +- ruff 1701 条 (E501×679 / W293×388 为主, F841×17, F401×15); B008 是 FastAPI 惯用法, 建议 ruff config 加 per-file-ignores +- pytest-asyncio `event_loop_policy` fixture deprecation 警告 43 条 + +## 测试基线 (token env 清空后) + +| 套件 | 结果 | +|---|---| +| tests/unit + tests/skills | 1355 passed, 5 skipped | +| tests/integration | 374 passed, 9 failed (上述 #2), 5 skipped | + +复跑命令 (host): +```powershell +cd D:\projects\opencli-admin +$env:API_AUTH_TOKEN=''; $env:AGENT_API_TOKEN=''; uv run --extra dev pytest tests/unit tests/integration -q --no-cov +``` diff --git a/tests/conftest.py b/tests/conftest.py index 3b2924e..842718f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,6 +10,13 @@ from httpx import ASGITransport, AsyncClient from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine +# Deployment config in the repo-root .env must not leak into tests: with +# API_AUTH_TOKEN set, every unauthenticated test request would 401 (fleet +# auth, ADR-0005). Clear before backend.main import snapshots settings — +# an empty env var wins over the .env file value in pydantic-settings. +os.environ["API_AUTH_TOKEN"] = "" +os.environ["AGENT_API_TOKEN"] = "" + from backend.auth import crypto from backend.database import Base, get_db from backend.main import app diff --git a/tests/integration/test_chat_api.py b/tests/integration/test_chat_api.py index 563355a..510eef6 100644 --- a/tests/integration/test_chat_api.py +++ b/tests/integration/test_chat_api.py @@ -130,3 +130,42 @@ async def test_confirm_update_provider_not_found(client): }, ) assert response.status_code == 404 + + +# ── confirm: trigger_task dispatch failure ─────────────────────────────────── +@pytest.mark.asyncio +async def test_confirm_trigger_task_reports_dispatch_failure(client, db_session, monkeypatch): + """Dispatch blowing up after the task row is committed must surface as 502, + not applied=True with a silently dead task.""" + from backend.models.source import DataSource + + source = DataSource( + name="Chat Trigger Source", + channel_type="rss", + channel_config={"feed_url": "https://example.com/feed.xml"}, + enabled=True, + ) + db_session.add(source) + await db_session.commit() + await db_session.refresh(source) + + class _BoomExecutor: + async def dispatch_collection(self, task_id: str, parameters: dict) -> dict: + raise RuntimeError("broker down") + + monkeypatch.setattr("backend.executor.get_executor", lambda: _BoomExecutor()) + + response = await client.post( + "/api/v1/chat/confirm", + json={ + "proposal": { + "tool": "trigger_task", + "args": {"source_id": source.id}, + "summary": "触发采集", + "diff": "", + } + }, + ) + + assert response.status_code == 502 + assert "派发失败" in response.json()["detail"]