-
Notifications
You must be signed in to change notification settings - Fork 1
fix(chat): surface trigger_task dispatch failure + test env isolation #17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Comment on lines
+439
to
+445
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 当任务派发失败时,虽然 API 抛出了 502 异常,但由于 建议在捕获到异常时,将 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)
task.status = "failed"
await db.commit()
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", "")) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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` + 原因 | ||
|
Comment on lines
+26
to
+28
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Mark the dispatch issue as resolved. This entry still documents the pre-fix behavior ( 🤖 Prompt for AI Agents |
||
|
|
||
| ## 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=''` | ||
|
Comment on lines
+32
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Document both authentication variables in test isolation. The remediation currently names only 🤖 Prompt for AI Agents |
||
|
|
||
| ### 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 | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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"] | ||||||||||||||||||||||
|
Comment on lines
+170
to
+171
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 为了确保在派发失败时,数据库中的任务状态确实被正确更新为了
Suggested change
|
||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
State loss and GC hazards in
LocalExecutor.While reviewing the
get_executor().dispatch_collection()invocation, I noticed two critical concurrency and state issues in theLocalExecutorimplementation provided in the context snippets:get_executor:get_executor()returns a new instance ofLocalExecutoron every call. BecauseLocalExecutorrelies on an instance-level_acquisition_tasksdictionary to track tasks, returning a new instance each time means previously dispatched tasks cannot be found or canceled later viacancel_acquisition.LocalExecutor.dispatch_collectionfires and forgets the collection task (t = asyncio.create_task(...)) without keeping a strong reference. The Python event loop only keeps weak references to tasks; this means active collection pipelines can be silently garbage-collected mid-execution when they yield to I/O.Please consider applying
@functools.lru_cachetoget_executorinbackend/executor/__init__.pyto preserve the executor state, and updatingLocalExecutor.dispatch_collectionto track its spawned tasks in an instance dictionary (similar to_acquisition_tasks) to prevent them from vanishing.🤖 Prompt for AI Agents