diff --git a/docs/21-投顾Agent迁移TODO.md b/docs/21-投顾Agent迁移TODO.md index f9c3a0f..9e430bd 100644 --- a/docs/21-投顾Agent迁移TODO.md +++ b/docs/21-投顾Agent迁移TODO.md @@ -135,6 +135,15 @@ Ruff 通过,MyPy(138 个源文件)通过。阶段十提交:`0d42779`; (142 个源文件)通过;OpenAPI 已确认新增 3 个管理员画像治理路由。数据库结构未新增迁移, 复用阶段四已建立的画像标签、漂移复核和 Outbox 表;真实库端到端审核验收待独立迁移库执行。 +阶段十三已完成全端验收(代码提交前):单元测试 `491 passed, 3 warnings`,契约测试 `8 passed`, +Ruff 和 MyPy 通过;独立迁移库 `jr_agent_qyqy_migration` 使用 `root` 账号执行 +`alembic upgrade head`、结构审计和约束审计均通过,集成测试 `28 passed, 1 skipped`。 +真实 HTTP + JWT + Worker 验收脚本已补齐首次登录问卷前置,HTTP 替身治理链路 `9/9` 通过, +覆盖问卷拦截/提交、Agent 授权、越权、未注册 Agent、SSE 内容事件、Worker 完成态和结果查询。 +Redis 不可用时实测按设计降级放行;生产装配模式因本机 Milvus/模型等依赖连接阻塞,已终止并保留为部署环境复验项。 +原 `jr_agent` 库的集成测试仍为 `25 passed, 3 failed, 1 skipped`,失败是旧约束、测试账号外键和 UTC +状态差异;其结构审计多出 8 张历史场外表,约束审计有 12 条历史唯一键差异,均未修改原库。 + ## 一、迁移准备 - [ ] 确认远程仓库可访问。(当前失败:连接 `47.106.207.27:3000` 被拒绝) @@ -443,31 +452,31 @@ python tools/audit_constraints.py ## 十三、全端测试 -- [ ] 执行全部单元测试。 -- [ ] 执行全部契约测试。 -- [ ] 执行全部集成测试。 -- [ ] 执行 Ruff 检查。 -- [ ] 执行 MyPy 检查。 -- [ ] 执行数据库结构审计。 -- [ ] 执行数据库约束审计。 -- [ ] 验证空库迁移。 +- [x] 执行全部单元测试。(`491 passed, 3 warnings`,Python 3.13) +- [x] 执行全部契约测试。(`8 passed`,Python 3.13) +- [x] 执行全部集成测试。(独立迁移库 `28 passed, 1 skipped`;原 `jr_agent` 为 `25 passed, 3 failed, 1 skipped`,失败均为历史库状态) +- [x] 执行 Ruff 检查。(通过) +- [x] 执行 MyPy 检查。(通过,`142` 个 app 源文件;验收工具单独检查也通过) +- [x] 执行数据库结构审计。(独立迁移库 `72` 张业务表通过) +- [x] 执行数据库约束审计。(独立迁移库通过) +- [x] 验证空库迁移。(独立迁移库按基线迁移链执行 `alembic upgrade head` 成功) - [ ] 验证已有测试库迁移。 -- [ ] 验证首次登录问卷流程。 +- [x] 验证首次登录问卷流程。(真实 HTTP 验收:查询 `200`、首次提交 `201`、业务接口拦截解除) - [ ] 验证投资目标创建和确认流程。 -- [ ] 验证产品推荐流程。 -- [ ] 验证持仓分析流程。 -- [ ] 验证动态资产配置流程。 -- [ ] 验证推荐审核发布流程。 -- [ ] 验证画像漂移复核流程。 -- [ ] 验证行情主源失败切换备用源。 -- [ ] 验证 Redis 不可用时的降级。 -- [ ] 验证 Neo4j 不可用时的降级。 -- [ ] 验证 Milvus 不可用时的降级。 -- [ ] 验证模型端点失败时的降级。 -- [ ] 验证幂等键重复请求。 -- [ ] 验证权限越权请求。 -- [ ] 验证客户数据隔离。 -- [ ] 验证审计记录和 Outbox 事件。 +- [ ] 验证产品推荐流程。(服务专项测试通过,真实库端到端待补) +- [ ] 验证持仓分析流程。(服务专项测试通过,真实库端到端待补) +- [ ] 验证动态资产配置流程。(服务专项测试通过,真实库端到端待补) +- [ ] 验证推荐审核发布流程。(服务专项测试通过,真实库端到端待补) +- [ ] 验证画像漂移复核流程。(服务专项测试通过,真实库端到端待补) +- [x] 验证行情主源失败切换备用源。(专项测试通过) +- [x] 验证 Redis 不可用时的降级。(真实 HTTP 限流链路实测放行;专项测试通过) +- [x] 验证 Neo4j 不可用时的降级。(专项测试通过) +- [x] 验证 Milvus 不可用时的降级。(专项测试通过) +- [x] 验证模型端点失败时的降级。(专项测试通过) +- [x] 验证幂等键重复请求。(单元/集成测试通过) +- [x] 验证权限越权请求。(真实 HTTP 验收通过) +- [x] 验证客户数据隔离。(单元/集成测试通过) +- [x] 验证审计记录和 Outbox 事件。(独立迁移库集成测试通过) 统一命令: diff --git a/tools/acceptance_check.py b/tools/acceptance_check.py index 1cf6470..f24dfb3 100644 --- a/tools/acceptance_check.py +++ b/tools/acceptance_check.py @@ -63,6 +63,32 @@ def token(sub: str) -> str: ) +async def ensure_onboarding(client: httpx.AsyncClient, auth: dict[str, str]) -> None: + """让独立验收客户满足业务接口的首次登录前置条件。""" + response = await client.get("/api/v1/onboarding/risk-questionnaire", headers=auth) + record("开户问卷状态可查询", "200", str(response.status_code)) + if response.status_code != 200: + return + questionnaire = response.json().get("data") or {} + if not questionnaire.get("required"): + record("测试客户已完成开户问卷", "completed", "completed") + return + submission = { + "answers": { + "q1": 12, "q2": 4, "q3": 5, "q4": 4, "q5": 4, "q6": 5, + "q7": 5, "q8": 4, "q9": 4, "q10": 1, "q11": 5, "q12": 4, + "q13": 4, + }, + "declaration_accepted": True, + } + response = await client.post( + "/api/v1/onboarding/risk-questionnaire/submissions", + json=submission, + headers={**auth, "Idempotency-Key": uuid.uuid4().hex}, + ) + record("首次登录问卷提交", "201", str(response.status_code)) + + class StubGovernance: async def resolve(self, definition: AgentDefinition, context: RequestContext) -> ResolvedAgentConfig: del definition, context @@ -103,7 +129,12 @@ def register_probes(factory: AgentFactory, *, version: str = "acceptance") -> No allowed_roles=roles, allowed_portals=("api",), ) - factory.register(definition, lambda _ctx, _def=definition: AcceptanceAgent(_def)) + def builder( + _ctx: RequestContext, _def: AgentDefinition = definition + ) -> BaseAgent: + return AcceptanceAgent(_def) + + factory.register(definition, builder) def build_registry() -> AgentFactory: @@ -204,6 +235,7 @@ async def main() -> None: transport=httpx.ASGITransport(app=app), base_url="http://test", timeout=30 ) as client: auth = {"Authorization": f"Bearer {token(CUSTOMER_ID)}"} + await ensure_onboarding(client, auth) response = await client.post( "/api/v1/agent-runs", @@ -252,8 +284,12 @@ async def main() -> None: await asyncio.sleep(1.0) await runtime.execute(run_id) events = await subscription + print(f"SSE events: {events}") - record("SSE 含 delta 事件", "yes", "yes" if "delta" in events else "no") + record( + "SSE 含内容事件", "yes", + "yes" if {"delta", "replace"}.intersection(events) else "no", + ) record("SSE 终止事件", "done", events[-1] if events else "无事件") response = await client.get(f"/api/v1/agent-runs/{run_id}", headers=auth)