diff --git a/docs/memory/MEMORY.md b/docs/memory/MEMORY.md index 6c41e22..624786a 100644 --- a/docs/memory/MEMORY.md +++ b/docs/memory/MEMORY.md @@ -30,7 +30,7 @@ - ✅ **Phase 6 还原已校验**:`core` 5 表按快照还原,**逐表 COUNT + CHECKSUM 与快照完全一致**(`95/59/73/19/0`)· `script_template` 页面可见恢复到 **20** - 🟢 **已拍板 2 项(用户 2026-09-13 定)**:**F-14 维持现状**(「目前够用就行」—— 接受 LLM 自撰风险提示措辞,**不扩** `compliance_guard.py:109` 名单;代价是措辞与是否出现由 LLM 随机决定,`06-chat.mjs` 中该条已降级为 2 条 INFO)· **RK 发现-1 关闭**(「这个不用管」—— 未知客户语义保持 officer 200 / advisor 403 / customer 403 **无一 404**,现状不泄漏客户存在性,仅作实测事实保留在风控包与横切包) - 🟢 **已拍板 · F-β = 选 A(用户 2026-09-13 定)**:**维持现状语义不动,下一轮只改测试**(`test_convert_confirm.py` 客户档位 C3→**C4** 或产品 R4→**R3**),**零业务代码改动**。**规则固化:`C3×R4`/`C4×R5` = 匹配但需揭示 ⇒ 自助渠道阻断并引导线下,这是设计意图不是缺陷**;**禁止给阻断响应体补 `forced_full_transfer` 让测试变绿**。上一轮 P1 修复使 `test_convert_confirm.py` **28 条失败**,**根因是测试前提过期,非接口缺陷**。依据:① L0 矩阵**刻意设三态**(`core_suitability_rule` 25 行实测)—— `C3×R4`/`C4×R5` = `allowed_with_disclosure`,服务层 `matched=True`/`blocked=False`/`mismatch_type="none"`/`block_code="SUIT_NEED_DISCLOSURE"`,**不是"不匹配"**,而是"匹配但须特别风险警示+签揭示书"(适当性办法 §19);② "需揭示→自助端阻断"是**已定设计**,三处同指:答辩稿 `答辩知识点清单.md:59`「**C3×R4 揭示 block**」· `CustomerTradePage.tsx:116`「标有『风险不匹配/需揭示』的请换产品或**到网点办理**」· `tradeEligibility.ts` 两类均 `canSelfServe:false`+`tone:'warn'`+标签「需揭示/**网点办理**」(揭示需双录/柜面,**线上无法完成"签署"动作**)⇒ P1 修复是**后端追平前端**;③ 28 条失败根因 = **测试前提过期**:`test_convert_confirm.py:1013` 的 `forced_full_transfer` 是**受理成功路径**字段,`CUST-CC`(C3) 被阻断后受理未发生 ⇒ `KeyError`;**阻断体里本就不该有该键**。**选项 B(已否决)**=把 PRD 二期(放行+提示)提前做,需新建前端揭示书 Modal + 留痕 + 后端受理分支;**只改后端不建前端更糟**。**🟠 新增记账(真缺口,只记录不修)**:文案引导的「网点办理/请联系持证投顾」**无实现通路** —— 理财师线**零交易 API**,全仓唯一入口 `POST /api/simulate/trade` 不区分渠道身份 ⇒ `C3×R4` 当前**任何渠道都买不成**,**文案承诺 > 实现能力**。处置二选一待定:① 答辩口径统一为"线下受理一期未实现";② 改前端文案(`CustomerTradePage.tsx:116`) -- 🔴 **F-ε(影响最广)**:**`pytest` 会写真实的 `jinrong_agent` 与 `jinrong_core`** —— 根因 `scripts/seed/import_script_templates.py:14` **模块级**绑定 `AgentSessionLocal`,而 `tests/test_sprint2_template_import.py` 未请求 sqlite fixture。实测:`script_template` 47→65 行、**20 条演示模板被翻成未审核(页面可见 20→6)**、`jinrong_core` 亦被写入。**本轮只还原,根因未修 ⇒ 再跑一次 pytest 污染即复现** +- 🟢 **F-ε(已修 · 2026-09-13 修复轮)**:`pytest` 写真库根因 = 测试文件**模块级** `from app.advisor_db import AgentSessionLocal` 在收集期绑定真库 sessionmaker,绕过 fixture 对 `app.advisor_db.AgentSessionLocal` 的 monkeypatch(`test_sprint1_compliance_rule_import.py` 早已用正确模式 `import app.advisor_db as advisor_db`)。**修法**:① 10 个 `test_sprint*.py` 改 `from app import advisor_db` + 调用点 `advisor_db.AgentSessionLocal`/`advisor_db.agent_engine`;② `import_script_templates.py` 同改;③ 唯一写真库的 subprocess 用例(seed 脚本独立跑)改 `@pytest.mark.skip`(另两处 subprocess `sync_template_vectors --dry-run` 只计数、`evaluate_compliance` 不带 trace_id/advisor_id 不写日志,经查只读)。**验证**:跑后 `script_template` 仍 65 / seed 20 / `test:*` 36,零污染。**同轮另发现并修**:6 个 `test_sprint2_*.py` 的 `token_for(username,password)` 打 `/api/v1/auth/login` + `{username,password}`,与现行 `POST /api/auth/login`(`actor_id`+`token_type`+`roles`)不符 ⇒ 迁移到 `tests/advisor_test_utils.login_token`。**副作用(如实)**:转 sqlite 后暴露 F-α 假 DDL 缺列(`agent_session` 无 `metadata` 等),sprint 组 45 条失败为陈旧 DDL/期望,非本轮引入 - 🔴 **F-ζ**:`--ignore-glob=test_sprint*.py` **是空操作**(pytest 匹配完整路径,缺 `*/`)⇒ 文档门槛"排除 sprint 后的 896"**从未按设计生效**。修正写法 `--ignore-glob='*/test_sprint*.py'`(1368→1274) - 🔴 **F-η**:测试套件**非幂等** —— 同库同命令**连跑两次 FAIL 集合不同**(129 vs 127 行)⇒ **单次 `pytest` 的 passed 数不可作"不退步"基线**,引用须附跑批序号 - ⚠️ **F-γ 更正**:Phase 0 记的 `core_share_lot=74` 与同次快照(**73 行**)**不自洽** ⇒ 还原判据改为「**与快照逐表 CHECKSUM 一致**」,不再用某个读数值 diff --git a/docs/memory/TODO.md b/docs/memory/TODO.md index ff8d5ed..1a9f651 100644 --- a/docs/memory/TODO.md +++ b/docs/memory/TODO.md @@ -57,7 +57,8 @@ **🔴 测试基建(与业务缺陷分开记账):** -- [ ] **F-ε(影响最广)· `pytest` 写真实库**:根因 `scripts/seed/import_script_templates.py:14` **模块级**绑定 `AgentSessionLocal`,而 `tests/test_sprint2_template_import.py` 未请求 `_advisor_agent_sqlite_db` fixture ⇒ 写真库且不回滚。实测:`script_template` **47→65 行**、**20 条演示模板被翻成未审核(页面可见 20→6)**、`jinrong_core` 亦被写入(`102/62/81/23/0` → `103/62/86/26/0`)。**本轮只还原**(`/d/tmp/e2e-gap/restore_script_template.py`)⇒ **再跑一次 pytest 污染即复现** +- [x] **F-ε(影响最广)· `pytest` 写真实库 → 已修(2026-09-13 修复轮)**:根因不单是 `import_script_templates.py:14`,**测试文件自身**的模块级 `from app.advisor_db import AgentSessionLocal` 在收集期绑定真库 sessionmaker,绕过 fixture 的 `monkeypatch`。**修法**:① 10 个 `test_sprint*.py` 改 `from app import advisor_db` + 调用点 `advisor_db.AgentSessionLocal`/`advisor_db.agent_engine`;② `import_script_templates.py` 同改;③ 唯一写真库的 subprocess 用例(seed 脚本独立跑)改 `@pytest.mark.skip`(另两处 subprocess 经查只读)。**验证**:跑后 `script_template` 仍 65/20/36,零污染。**副作用(如实)**:转 sqlite 后暴露 F-α 假 DDL 缺列(`agent_session` 无 `metadata` 等),45 条 sprint 失败为陈旧 DDL/期望,非本轮引入 +- [x] **陈旧登录契约(2026-09-13 修复轮新发现)**:6 个 `test_sprint2_*.py` 的 `token_for(username,password)` 打 `/api/v1/auth/login` + `{username,password}`,与现行 `POST /api/auth/login`(`actor_id`+`token_type`+`roles`)不符 ⇒ 404/422。迁移到 `tests/advisor_test_utils.login_token`(已有 compat 映射)。6 文件全改,无 `/api/v1` 残留 - [ ] **F-ζ · `--ignore-glob=test_sprint*.py` 是空操作**:pytest 匹配**完整路径**,`test_sprint*.py` 缺 `*/` ⇒ 一个文件都没过滤掉(1368 collected / 其中 94 来自 sprint)。**故文档门槛「排除 sprint 后的 896」从未按设计生效**。修正:`--ignore-glob='*/test_sprint*.py'`(1368 → **1274**,sprint 项 **0**) - [ ] **F-η · 套件非幂等**:同库同命令**连跑两次 FAIL 集合不同**(129 vs 127 行)⇒ **单次 `pytest` 的 passed 数不可作「不退步」基线**,引用须附跑批序号与库状态 - [ ] **F-α · 验收文档门槛陈旧**:`docs/答辩/DEMO-功能测试流程.md` A3 称 896 passed,实测 1236/102/9/21。根因:**假 DDL 与 fixture 插入不一致**(`test_platform_api.py` 21 条 error 全是 `table core_holding has no column named quantity`) @@ -131,8 +132,8 @@ - [x] 本机 Core convert 种子(`verify_convert_seed.py` + nav-history 可选)· `verify_convert_api.py` 9/9 - [x] 前端:`/app/risk/convert` + `web/src/api/convert.ts` - [x] **端到端首验([FE-001](tests/2026-09-12-agent-frontend-e2e/TEST-LOG-2026-09-12-FE-001.md) v1.0)**:后端四端点 R6 全绿(202 受理 · 幂等同 `group_id` · 未确认单不含 `_CONFIRMED_ONLY_FIELDS` · 撤单 409 语义 · 确认批处理双日期 · RBAC 403)· 前端页 19/21 PASS -- [ ] **修 F1**:`RiskConvertPage.tsx:16-27` 的 `ACCEPT_PRESETS[0]` 是**跨主体组合**(`110022` 华夏/TA-CN-001 → `161725` 易方/TA-CN-002)→ 点一下就 400 `CROSS_ENTITY_NOT_SUPPORTED`。**演示路径的唯一入口不可用,建议优先修**(实测换 `PROD-000001` 即 202) -- [ ] **修 F4**:`RiskConvertPage.tsx:37` 的 `confirmDate` 默认 `2026-09-15` 落在 NAV 区间外(最新 `2026-09-11`)→ 一进来就走 `confirmed=0` 的降级路径。改为数据驱动或退化为 `2026-09-11` +- [x] **修 F1**(已由 `2296a4a` 修复):`ACCEPT_PRESETS[0]` 已改为**同主体** `PROD-110022 → PROD-000001`(华夏/TA-CN-001),不再触发 `CROSS_ENTITY_NOT_SUPPORTED` +- [x] **修 F4**(已由 `2296a4a` 修复):`FALLBACK_CONFIRM_DATE` 退化为 `2026-09-11` + 新增 `convert_meta_api` 取最新净值日 - [ ] 全量 pytest 回归 **2026-09-12 · 投资顾问 Agent 合并**(分支 **`integrate/advisor-agent`** · 目标合入 **`merger`**) @@ -145,7 +146,7 @@ - [x] **顾问线真实 HTTP 端到端验收**([TEST-2026-09-12-ADV-001](tests/2026-09-12-advisor-agent-e2e/TEST-LOG-2026-09-12-ADV-001.md) **v1.1**,2026-09-12 复跑改写):63 用例 **60 PASS / 3 FAIL / 0 SKIP** → **D1 已关闭** ✅ · D2/D3 待修 - **D1 已由 `03fce88` 修复**:`app/repository/core_ro.py:839,851` 补齐 `list_product_nav_history` / `list_latest_product_navs`,与 `app/service/market_data_service.py:40,61` 调用一致。实测 G5 市场异动 **10/10 PASS**(`scanned=14 created=2 used_fallback=False`)+ 浏览器 4/4 转绿 - **D3 定性已更正**:原假设「高层 `MilvusClient` 自动 load,所以不是未 load 的问题」**被服务端日志证伪** —— uvicorn 日志 `MilvusException (code=101, … Collection 'kb_script_templates' is in state 'released'; call load() before search/get/query)` 出现 **10 次**。**第二层**:集合目录 5.0K/2 文件 vs 已灌集合 50K/5 文件 ⇒ **疑似同时是空集**,仅补 `load()` 未必恢复召回 -- [ ] **修 D2**:`app/model/entities.py:116` 的 `created_at` 无 Python 侧 default,`kyc_session_repository` 用 ORM 写且不传该字段 ⇒ 显式 NULL 覆盖 DDL 默认值 → `IntegrityError 1048`。前端取证:KYC 页「创建会话」成功,「发送采集」报 `INTERNAL_ERROR` +- [x] **修 D2**(已由 `2296a4a` 修复):`AgentMessage.created_at`(`entities.py:116`)补 `default=lambda: datetime.now(timezone.utc)...`;`KycSession.created_at`(`entities_advisor.py:45`)已有 `server_default=func.now()`。原 `IntegrityError 1048` 根因消除 - [ ] **修 D3**:先确认 `kb_script_templates` 是否已灌向量(`sync_template_vectors.py`),再定 `load()` 修法 - [ ] **`test_sprint*` 全绿**(conftest 内存 SQLite + 规则种子已接;模板 seed · Core 行情 · Milvus 等待补)—— **注意**:sprint 测试全部 fake 掉 Core 提供方,**拦不住 D1 这类接缝错配**,不能替代真实端到端 - [x] 合入 **`merger`**(2026-09-12 · fast-forward `973bf1b` → `4be27c5`) @@ -302,7 +303,7 @@ **复跑新暴露的三条(需拍板 · 均为「修复本身的残留」,非新缺陷)** -- [ ] **P0 只堵了方向①(建议补做,低成本防回归)**:`infer_roles()` 已不信任入参,但 `/api/auth/login`(`app/api/auth.py:16`)**仍无 `app_env` 门禁**、`app/main.py:103` **仍无条件挂载**。本轮只证明「注入 `roles` 不再生效」,**未证明「生产配置下非白名单 actor 一律无法登录」** —— 若日后有人改回 `if roles` 语义,缺陷原样复现 +- [ ] **P0 只堵了方向①(🟢 2026-09-13 已拍板「本轮不动,只记录」)**:`infer_roles()` 已不信任入参,但 `/api/auth/login`(`app/api/auth.py:16`)**仍无 `app_env` 门禁**、`app/main.py:103` **仍无条件挂载**。本轮只证明「注入 `roles` 不再生效」,**未证明「生产配置下非白名单 actor 一律无法登录」** —— 若日后有人改回 `if roles` 语义,缺陷原样复现。**处置(已拍板)**:本轮不动,留待部署前修(可选:login() 内加 `app_env != "development"` 即 403/404,或仅 development 条件挂载 router)。当前 `APP_ENV=development`,加门禁不影响 demo。 - [ ] **P1 可能收窄了业务能力(必须业务确认)**:`simulate_self_service_blocked` 把 `allowed_with_disclosure` 并入阻断 ⇒ **R4 产品自助通道完全不可下单**。原设计是「可买但需弹揭示书 + 双录」。当前实现与前端 `tradeEligibility.ts` 口径一致,但**等于收窄业务能力** —— 若期望「揭示后仍可买」,本修复是过渡方案,需另开揭示书续接流程 - [ ] **`CT3-05` 跨轮次顺序敏感(测试资产设计缺陷)**:查询窗口是 `audit_log.created_at >= DATE_SUB(NOW(), INTERVAL 15 MINUTE)`(**墙钟**),而 `jinrong_agent` 按审计语义**永不还原** ⇒ **距上一轮 <15 分钟复跑会假 FAIL**(本轮实测踩到 `3 条`残留,等待窗口老化后才取得 `0 条`)。建议改为按**本轮 runstamp** 过滤,消除跨轮次串扰。**复跑口径已写入 README 与 `_raw/README.md`** - [ ] **Redis 降级路径未验证**:停 Redis 复跑 CT11(`restore_flow_from_context`)**两轮均未执行** —— 唯一一条**主口径未覆盖**的功能路径 diff --git a/docs/memory/tests/2026-09-13-crosscut-e2e/README.md b/docs/memory/tests/2026-09-13-crosscut-e2e/README.md index 98f8ff1..f15c37a 100644 --- a/docs/memory/tests/2026-09-13-crosscut-e2e/README.md +++ b/docs/memory/tests/2026-09-13-crosscut-e2e/README.md @@ -152,6 +152,7 @@ **本轮复跑实测 49 passed / 28 failed,与 Phase 0 完全一致** ⇒ **证明是既有问题,不是本轮引入**。这正好实证了上一轮 `MEMORY.md` 里「⚠️ 复跑新发现(需拍板)」的第 ② 条:**「P1 修复可能收窄了业务能力」**。 → **只报告不修**。**更新 2026-09-13:此项已查清** —— 业务上 R4 的"需揭示 → 自助端阻断"**方向正确**(L0 矩阵刻意设三态 + 答辩稿「C3×R4 揭示 block」+ 前端 `canSelfServe:false`),**28 条失败是测试前提过期而非接口缺陷**。完整依据、建议与反向禁忌见 **§8.2**。 +→ **✅ 已修(2026-09-13 修复轮 · 选 A)**:`test_convert_confirm.py` 客户档位 C3→C4(`C4×R4=allowed`),四文件 **77 passed / 0 failed**;**零业务代码改动**,未补 `forced_full_transfer`(遵守反向禁忌)。 #### **F-γ —— `core_share_lot` 的基线读数与快照不符(**本轮更正口径**) @@ -185,6 +186,8 @@ > **纪律说明:** 探针写进**真库**而非 sqlite 假库,是本项目"19 个 `test_sprint*.py` 伪造全部 Core 数据源"的**反面同源问题** —— 有的是"假数据源导致测不出接缝",有的是"真数据源被测试写坏"。两者都值得在修复轮立项。 +> **✅ 已修(2026-09-13 修复轮)**:根因实为测试文件**模块级** `from app.advisor_db import AgentSessionLocal` 在收集期绑定真库 sessionmaker、绕过 fixture 的 `monkeypatch`(不单是 `import_script_templates.py:14`)。修法:① 10 个 `test_sprint*.py` 改 `from app import advisor_db` + 调用点 `advisor_db.AgentSessionLocal`/`advisor_db.agent_engine`;② `import_script_templates.py` 同改;③ 唯一写真库的 subprocess 用例(seed 脚本独立跑)改 `@pytest.mark.skip`(另两处 subprocess `sync_template_vectors --dry-run` / `evaluate_compliance` 经查**只读**)。**验证**:跑后 `script_template` 仍 **65 / seed 20 / `test:*` 36**,零污染。**同轮另修**:6 个 `test_sprint2_*.py` 的陈旧登录契约(`/api/v1/auth/login` + `{username,password}` → `tests/advisor_test_utils.login_token`)。**副作用(如实)**:转 sqlite 后暴露 **F-α 假 DDL 缺列**(`agent_session` 无 `metadata` 等),sprint 组 45 条失败为陈旧 DDL/期望,非本轮引入。 + #### **F-ζ —— `--ignore-glob=test_sprint*.py` 是**空操作**(本轮新发现)** `docs/答辩/DEMO-功能测试流程.md` A3 的门槛命令写着 `pytest -q --ignore-glob=test_sprint*.py`。**实测该 flag 一个文件都没过滤掉:** @@ -286,6 +289,7 @@ > **拍板结论:维持现状语义不动,下一轮只改测试(客户档位 C3→C4 或产品 R4→R3),零业务代码改动。** > **规则固化(答辩口径同此)**:`C3×R4` / `C4×R5` = **匹配但需揭示** ⇒ **自助渠道阻断并引导线下**,这是**设计意图,不是缺陷**。 > **禁止项**:不得给阻断响应体补 `forced_full_transfer` 让测试变绿(=让"阻断"伪装成"受理成功")。选项 B(做揭示书 Modal)**已否决**,若日后要做属独立需求轮。 +> **✅ 已执行(2026-09-13 修复轮)**:`test_convert_confirm.py` 客户档位 **C3→C4**(`C4×R4=allowed`),四文件 **77 passed / 0 failed**;`CUST_LOW`(C1) 阻断用例保留作 T+1 复核对照;阻断响应体仍**不含** `forced_full_transfer`。 以下为支撑该结论的原始取证(存档备答辩追问): @@ -352,6 +356,7 @@ python -m pytest -q --ignore-glob='*/test_sprint*.py' | 2 | **`/app/home` 与 `/app` 两个路由未访问** | 同上,XC2 未执行。`08-edge` 只验了"未知路由重定向到角色首页",**不等于**访问过 `/app/home` 本身 | | 3 | **XC3 的面包屑 / 登出 / 会话侧栏切换未做浏览器验证** | GX4 只从 **HTTP 侧**验了 `close-all`/`sessions`/`{id}/close`;**前端按钮点击路径未验** | | 4 | **测试套件不可复现(F-η)** | 单次 `pytest` 结果随库状态漂移。**引用其数字必须附跑批序号与库状态** | -| 5 | **F-ε 的根因仍在** | 本轮只**还原**,未修 `import_script_templates.py` 的**模块级 session 绑定**。**再跑一次 pytest 污染即复现** | -| 6 | **28 条交易单测失败未消解** | 根因已查清=**测试前提过期**(非接口缺陷);修法已给(改测试客户档位 C3→C4 / 产品 R4→R3),**待点头后下一轮执行,不在本轮范围**。见 §8.2 | +| 5 | ~~F-ε 根因仍在~~ **已修(2026-09-13 修复轮)** | 10 个 `test_sprint*.py` 改模块属性访问 + skip 写真库 subprocess 用例;验证零污染(`script_template` 65/20/36 前后一致)。**副作用**:转 sqlite 后暴露 F-α 假 DDL 缺列(见 §6.2) | +| 6 | ~~28 条交易单测失败未消解~~ **已修** | 选 A 已执行(客户档位 C3→C4),四文件 **77/0**。见 §8.2 | +| 7 | **P0 mock 登录无 `app_env` 门禁** | 已拍板「本轮不动,只记录」(2026-09-13)。`/api/auth/login`(`app/api/auth.py:16`)无条件挂载 ⇒ 生产也能签白名单角色 token。**留待部署前修**:`login()` 内 `app_env != "development"` 即 403/404,或仅 development 条件挂载 router。当前 `APP_ENV=development`,加门禁不影响 demo | | 7 | `GX4-07`/`GX4-08` 两条状态机/越权路径未覆盖 | 见 §三 SKIP 说明 | diff --git a/docs/memory/tests/2026-09-13-crosscut-e2e/TEST-LOG-2026-09-13-XC-001.md b/docs/memory/tests/2026-09-13-crosscut-e2e/TEST-LOG-2026-09-13-XC-001.md index e2923a2..0240036 100644 --- a/docs/memory/tests/2026-09-13-crosscut-e2e/TEST-LOG-2026-09-13-XC-001.md +++ b/docs/memory/tests/2026-09-13-crosscut-e2e/TEST-LOG-2026-09-13-XC-001.md @@ -9,8 +9,8 @@ | 文档版本 | v1.0 | | 创建日期 | 2026-09-13 | | 关联分支 | `merger` | -| 关联拍板/TODO | **已收敛(2026-09-13 用户拍板)**:**F-14 维持现状** · **RK 发现-1 关闭** · **F-β 已查清待点头**(结论:现状"阻断"方向正确,缺陷在测试侧;见 README §8.2) | -| 风险等级 | **MEDIUM**(F-β 的 28 条单测失败**未消解**;F-ε 根因仍在;套件非幂等) | +| 关联拍板/TODO | **已收敛 + 修复轮落地(2026-09-13)**:**F-14 维持现状** · **RK 发现-1 关闭** · **F-β 选 A 并已执行**(测试档位 C3→C4,77/0)· **F-ε 已修**(零污染验证)· **P0 mock 登录门禁「本轮不动,只记录」** | +| 风险等级 | **MEDIUM**(F-β / F-ε 已修;仍开放:F-α 假 DDL 缺列 · F-η 套件非幂等 · P0 mock 登录无门禁[defer] · 「网点办理」无通路) | | 缺陷类型 | 测试基建(腐化/污染/非幂等)· 文档门槛陈旧 · 编号歧义 · 数据还原 | | 发现阶段 | 端到端覆盖缺口补测轮 · Part A/B/C + Phase 6 | @@ -74,8 +74,8 @@ | 复现步骤 | 见 §5 执行记录第 4~6 条与 §6 数据准备 | | 期望 | 测试隔离,不写真实业务库;同一条命令连跑结果一致 | | 实际 | ① `pytest` 后 `script_template` **47 → 65 行**,20 条演示模板被翻成 `is_approved=0`,**页面可见 20 → 6**;② `jinrong_core` 5 表亦被写入(`102/62/81/23/0` → `103/62/86/26/0`);③ **连跑两次的 FAIL/ERROR 集合不同**(129 行 vs 127 行) | -| 根因 | `scripts/seed/import_script_templates.py:14` 在**模块级**绑定 `AgentSessionLocal`,而 `tests/test_sprint2_template_import.py` 的 seed 调用**未请求** `_advisor_agent_sqlite_db` fixture ⇒ 写真库且不回滚 | -| 修复方案 | **本轮只还原,不改根因**(属测试基建立项)。**还原已执行且可复现**:见 §6。**⚠️ 再跑一次 pytest 污染即复现** | +| 根因 | 测试文件**模块级** `from app.advisor_db import AgentSessionLocal` 在收集期绑定真库 sessionmaker,绕过 fixture 对 `app.advisor_db.AgentSessionLocal` 的 monkeypatch(不单是 `import_script_templates.py:14` 一处);另有 seed 脚本独立 subprocess 写真库 | +| 修复方案 | **已修(2026-09-13 修复轮)**:① 10 个 `test_sprint*.py` 改 `from app import advisor_db` + `advisor_db.AgentSessionLocal`/`advisor_db.agent_engine`;② `import_script_templates.py` 同改;③ 唯一写真库的 subprocess 用例(seed 脚本独立跑)改 `@pytest.mark.skip`(另两处 subprocess `sync_template_vectors --dry-run` / `evaluate_compliance` 经查只读)。**验证**:跑后 `script_template` 仍 65/20/36,零污染 | ## 4. 变更清单 @@ -99,9 +99,10 @@ | **F-05** | `scripts/e2e/09/10/11` harness 指向 | 仓库内外 harness **已分叉**(9301 B vs 6049 B) | 逐支改指向 `scripts/e2e/harness.mjs`,核对原语;**先跑通一支再收下一支**(R8) | ✅ 已修 | | **F-06** | 截图路径 | `_raw/shots/` 固定常量 ⇒ 四包互相顶掉(上轮已丢 v1.0 截图) | **零代码改动**:用 `E2E_SHOT_DIR` env(`harness.mjs:39` 早已支持) | ✅ 已修 | | **F-α** | `docs/答辩/…` A3 门槛 | 称 `896 passed`,实测 1236 | 只报告;以实测为基线 | 📝 记录 | -| **F-β** | `tests/test_convert_confirm.py` | 固化旧语义 ⇒ `KeyError: forced_full_transfer`,28 条失败 | **只报告**。**已查清(2026-09-13)**:根因是**测试前提过期**(`forced_full_transfer` 属受理成功路径字段,C3 被阻断后受理未发生),**非接口缺陷**;建议下一轮改测试客户档位 C3→C4 / 产品 R4→R3。**⚠️ 不得给阻断响应体补该键**。见 README §8.2 | 📝 记录 | +| **F-β** | `tests/test_convert_confirm.py` | 固化旧语义 ⇒ `KeyError: forced_full_transfer`,28 条失败 | **已修(选 A)**:测试客户档位 C3→C4(`C4×R4=allowed`),四文件 **77 passed / 0 failed**。零业务代码改动,未补 `forced_full_transfer`。见 README §8.2 | ✅ 已修 | | **F-γ** | Phase 0 基线口径 | "74"读数与该次快照(73 行)不自洽 | 还原判据改为**与快照逐表 CHECKSUM 一致** | ✅ 已更正 | -| **F-ε** | `scripts/seed/import_script_templates.py:14` | 模块级 session 绑定 ⇒ pytest 写真库 | **本轮只还原**(根因未修) | ⚠️ 已还原 | +| **F-ε** | 10 个 `test_sprint*.py` + `import_script_templates.py` | 模块级 `from app.advisor_db import AgentSessionLocal` 收集期绑真库 ⇒ pytest 写真库 | **已修**:改模块属性访问 `advisor_db.AgentSessionLocal` + skip 写真库 subprocess 用例。验证零污染(`script_template` 65/20/36 前后一致) | ✅ 已修 | +| **陈旧登录契约** | 6 个 `test_sprint2_*.py` 的 `token_for` | 打 `/api/v1/auth/login` + `{username,password}`,与现行 `POST /api/auth/login`(`actor_id`+`token_type`+`roles`)不符 ⇒ 404/422 | **已修**:迁移到 `tests/advisor_test_utils.login_token`(已有 compat 映射)。6 文件全改,无 `/api/v1` 残留 | ✅ 已修 | | **F-ζ** | 门槛命令 | `--ignore-glob=test_sprint*.py` 为空操作 | 只报告;给出修正写法 | 📝 记录 | | **F-η** | 测试套件 | 非幂等:连跑两次 FAIL 集合不同 | 只报告;引用数字须附跑批序号 | 📝 记录 | @@ -155,12 +156,13 @@ | 项 | 结论 | | --- | --- | | 需求覆盖 | 横切面:**孤立路由 5 组**(含★零覆盖的 `threshold-check`)· **浏览器跨角色/响应式/对话/边界 4 支** · **测试基建台账 F-α…F-η + F-01…F-06** · **Phase 6 数据还原校验** | -| 回归 | ⚠️ **部分退步(但非本轮引入)**:`npm run build` ✅ 退出码 0 · `npm run test` ✅ 27 passed · **四文件交易单测 ❌ 49/28(F-β,与 Phase 0 完全一致)** · `pytest` ❌ 1236/102/9/21(F-α + F-ζ) | -| 剩余风险 | ① **F-β 的 28 条单测失败未消解** —— 根因已查清为**测试前提过期**(非接口缺陷),待点头后下一轮改测试(README §8.2);② **F-ε 根因仍在**(再跑 pytest 污染即复现);③ **F-η 套件非幂等** ⇒ 单次 passed 数不可作可靠基线;④ `/register` 等三项前端覆盖**本轮未做**(见 README §十一);⑤ **新增:文案引导的「网点办理/请联系持证投顾」无实现通路**(理财师线零交易 API)—— 见 README §8.3 | -| 建议人工再验 | ① `python scripts/dev/gap_routes_e2e_smoke.py` 目视 `threshold-check` 三角色返回;② 打开 `/app/risk/home` 确认 `pending_review=35` 下 KPI 自洽;③ **拍板 F-β / F-14 / RK 发现-1 三项**后再开修复轮 | +| 回归 | ✅ **修复轮后(2026-09-13)**:四文件交易单测 **77/0**(F-β 已修)· `test_sprint2_template_import.py` **3/0/1 skip**(F-ε 已修)· `pytest` 非 sprint **1199/45/9/21**(F-α 假 DDL 残留 + F-ζ 修正写法)· `npm run build`/`npm run test` ✅ 未退步 | +| 剩余风险 | ① ~~F-β~~ ~~F-ε~~ **已修(修复轮)**;② **F-α 假 DDL 缺列**(`agent_session` 无 `metadata` 等)→ sprint 组 45 条失败为陈旧 DDL/期望,**只记录**;③ **F-η 套件非幂等** ⇒ 单次 passed 数不可作可靠基线;④ `/register` 等三项前端覆盖**未做**(见 README §十一);⑤ **新增:文案「网点办理/请联系持证投顾」无实现通路**(理财师线零交易 API)—— 见 README §8.3;⑥ **P0 mock 登录无 `app_env` 门禁**(已拍板「本轮不动,只记录」,留待部署前修) | +| 建议人工再验 | ① `python scripts/dev/gap_routes_e2e_smoke.py` 目视 `threshold-check` 三角色返回;② 打开 `/app/risk/home` 确认 `pending_review=35` 下 KPI 自洽;③ F-β/F-14/RK 发现-1 已拍板并**修复轮已执行**;④ 部署前补 P0 mock 登录门禁 | ## 8. 签核 / 修订历史 | 版本 | 日期 | 修订人 | 说明 | | --- | --- | --- | --- | | v1.0 | 2026-09-13 | 本轮补测 | 首次建立:GX1–GX5 + Part B 四支 + Part C 门槛 + 测试基建台账 + Phase 6 还原校验 | +| v1.1 | 2026-09-13 | 修复轮 | F-β 选 A 已执行(C3→C4,77/0)· F-ε 已修(模块属性访问 + skip subprocess,零污染)· 6 个 sprint2 陈旧登录契约已迁移 · P0 mock 登录门禁「本轮不动,只记录」 | diff --git a/docs/superpowers/plans/2026-09-13-e2e-coverage-gap.md b/docs/superpowers/plans/2026-09-13-e2e-coverage-gap.md index fe32799..f3be311 100644 --- a/docs/superpowers/plans/2026-09-13-e2e-coverage-gap.md +++ b/docs/superpowers/plans/2026-09-13-e2e-coverage-gap.md @@ -3,6 +3,8 @@ > 方法:superpowers 的 verification-before-completion(先取证再下结论)· systematic-debugging(先定根因再提修法)· dispatching-parallel-agents(独立域并行)· writing-plans(本计划)。 > 执行时把本计划归档副本写入 `docs/superpowers/plans/2026-09-13-e2e-coverage-gap.md`。 +> **📌 修复轮跟进(2026-09-13 晚):** 本计划原口径「缺陷只报告不修」,用户随后拍板「开始修吧」⇒ 对可修项**开修复轮**,已落地:**F-β 选 A 已执行**(`test_convert_confirm.py` C3→C4,四文件 77/0)· **F-ε 已修**(10 个 `test_sprint*.py` 模块属性访问 + skip 写真库 subprocess,零污染)· **6 个 sprint2 陈旧登录契约已迁移** · **P0 mock 登录门禁「本轮不动,只记录」**。详见 `docs/memory/tests/2026-09-13-crosscut-e2e/`。 + ## Context **为什么做这一轮:** 上一轮(`TEST-2026-09-13-CT-001`)刚把**客户线交易**做成全量端到端——真浏览器 + 真 HTTP + 真库对账,Part A `135 PASS / 0 FAIL / 1 SKIP / 25 INFO`(退出码 0)、Part B `41 PASS / 0 FAIL / 0 SKIP / 7 INFO`(退出码 0)。 diff --git a/scripts/seed/import_script_templates.py b/scripts/seed/import_script_templates.py index 481c045..787b105 100644 --- a/scripts/seed/import_script_templates.py +++ b/scripts/seed/import_script_templates.py @@ -11,7 +11,7 @@ from pathlib import Path ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) -from app.advisor_db import AgentSessionLocal +from app import advisor_db from app.model.entities_advisor import ScriptTemplate from app.model.advisor_schemas import ComplianceCheckRequest from app.service.compliance_check_service import ComplianceCheckService @@ -98,7 +98,7 @@ def import_templates_from_markdown( created = 0 updated = 0 - with AgentSessionLocal() as session: + with advisor_db.AgentSessionLocal() as session: for seed_template in templates: check_result = compliance_service.check_text( ComplianceCheckRequest(text=seed_template.content, scene=seed_template.scene) diff --git a/tests/test_convert_confirm.py b/tests/test_convert_confirm.py index 4573125..4d21819 100644 --- a/tests/test_convert_confirm.py +++ b/tests/test_convert_confirm.py @@ -39,7 +39,7 @@ from app.service.convert.errors import ( ) from app.service.risk.rules import RiskThresholds -CUST = "CUST-CC" # C3 客户 → 转入 R4 = 放行 +CUST = "CUST-CC" # C4 客户 → 转入 R4 = 放行(C4×R4=allowed) CUST_LOW = "CUST-CCL" # C1 客户 → 转入 R4 = 阻断(T+1 复核用) PROD_OUT = "PROD-CC-OUT" PROD_IN = "PROD-CC-IN" @@ -98,7 +98,7 @@ def _seed(engine, *, with_nav: bool = True, min_hold: str = "0") -> None: "VALUES (:c, 'CC低风险', 40, 1)", c=CUST_LOW, ) - for cid, code in ((CUST, "C3"), (CUST_LOW, "C1")): + for cid, code in ((CUST, "C4"), (CUST_LOW, "C1")): _exec( engine, "INSERT INTO core_customer_risk (customer_id, risk_code, evaluated_at, expires_at) " @@ -548,11 +548,11 @@ def test_confirm_nav_pending_retry_does_not_false_conflict(sqlite_engine): def test_confirm_suitability_recheck_rejects_and_releases(sqlite_engine): """受理通过、T+1 复核不通过 → `rejected` + 占用释放、份额不变、无流水。""" _seed(sqlite_engine) - # 该客户在受理时**合规**(C3 且风评在有效期内)→ 受理通过; + # 该客户在受理时**合规**(C4 且风评在有效期内)→ 受理通过; # 随后风评到期并降级为 C1(转入 R4 股基即不匹配),模拟 D25 的 T+1 复核场景。 _exec( sqlite_engine, - "UPDATE core_customer_risk SET risk_code = 'C3', evaluated_at = :t, expires_at = :e " + "UPDATE core_customer_risk SET risk_code = 'C4', evaluated_at = :t, expires_at = :e " "WHERE customer_id = :c", c=CUST_LOW, t=SUBMIT_AT - timedelta(days=30), e=SUBMIT_AT + timedelta(days=300), ) diff --git a/tests/test_sprint0_infrastructure.py b/tests/test_sprint0_infrastructure.py index 74ccbc5..cb78a3e 100644 --- a/tests/test_sprint0_infrastructure.py +++ b/tests/test_sprint0_infrastructure.py @@ -3,7 +3,7 @@ from uuid import uuid4 import pytest -from app.advisor_db import AgentSessionLocal +from app import advisor_db from app.advisor_exceptions import OwnershipDeniedError from app.api.advisor_auth_adapter import AdvisorAuthContext from app.model.entities import AuditLog @@ -126,7 +126,7 @@ def test_sqlalchemy_audit_repository_persists_append_only_event(): trace_id = f"trace-db-{uuid4().hex}" repository_class = getattr(audit_module, "SqlAlchemyAuditRepository", None) assert repository_class is not None - repository = repository_class(AgentSessionLocal) + repository = repository_class(advisor_db.AgentSessionLocal) service = AuditService(repository) service.record( @@ -137,7 +137,7 @@ def test_sqlalchemy_audit_repository_persists_append_only_event(): input_summary={"source": "pytest"}, ) - with AgentSessionLocal() as session: + with advisor_db.AgentSessionLocal() as session: event = session.query(AuditLog).filter(AuditLog.trace_id == trace_id).one() assert event.event_type == "audit_repository_test" @@ -185,7 +185,7 @@ def test_trace_id_header_is_reused_in_response_and_rbac_audit_event(): assert response.headers["X-Trace-Id"] == trace_id assert response.json()["trace_id"] == trace_id - with AgentSessionLocal() as session: + with advisor_db.AgentSessionLocal() as session: event = ( session.query(AuditLog) .filter(AuditLog.trace_id == trace_id, AuditLog.event_type == "rbac_denied") diff --git a/tests/test_sprint1_compliance_check_log.py b/tests/test_sprint1_compliance_check_log.py index e3d4bc0..c79a6c7 100644 --- a/tests/test_sprint1_compliance_check_log.py +++ b/tests/test_sprint1_compliance_check_log.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from app.advisor_db import AgentSessionLocal +from app import advisor_db from app.main import app from app.model.entities_advisor import ComplianceCheckLog from app.model.advisor_schemas import ComplianceCheckRequest @@ -20,7 +20,7 @@ def advisor_token() -> str: return response.json()["data"]["access_token"] def get_check_log(trace_id: str) -> ComplianceCheckLog: - with AgentSessionLocal() as session: + with advisor_db.AgentSessionLocal() as session: log = ( session.query(ComplianceCheckLog) .filter(ComplianceCheckLog.trace_id == trace_id) diff --git a/tests/test_sprint1_copy_track.py b/tests/test_sprint1_copy_track.py index 34f343c..fbec12f 100644 --- a/tests/test_sprint1_copy_track.py +++ b/tests/test_sprint1_copy_track.py @@ -4,7 +4,7 @@ from uuid import uuid4 from fastapi.testclient import TestClient from sqlalchemy import inspect -from app.advisor_db import AgentSessionLocal, agent_engine +from app import advisor_db from app.main import app from app.model.entities import AuditLog from app.model.entities_advisor import CopyTrackLog @@ -41,7 +41,7 @@ def track_payload(*, content: str, check_result: dict, risk_level: str, warn_con } def latest_copy_track(trace_id: str) -> CopyTrackLog: - with AgentSessionLocal() as session: + with advisor_db.AgentSessionLocal() as session: row = ( session.query(CopyTrackLog) .filter(CopyTrackLog.trace_id == trace_id) @@ -53,7 +53,7 @@ def latest_copy_track(trace_id: str) -> CopyTrackLog: return row def latest_copy_audit(trace_id: str) -> AuditLog: - with AgentSessionLocal() as session: + with advisor_db.AgentSessionLocal() as session: row = ( session.query(AuditLog) .filter(AuditLog.trace_id == trace_id, AuditLog.event_type == "copy_track") @@ -65,7 +65,7 @@ def latest_copy_audit(trace_id: str) -> AuditLog: return row def test_copy_track_migration_creates_table(): - inspector = inspect(agent_engine) + inspector = inspect(advisor_db.agent_engine) assert "copy_track_log" in inspector.get_table_names() columns = {column["name"] for column in inspector.get_columns("copy_track_log")} diff --git a/tests/test_sprint2_market_alert_feedback.py b/tests/test_sprint2_market_alert_feedback.py index bc4dbd6..fb9c6ef 100644 --- a/tests/test_sprint2_market_alert_feedback.py +++ b/tests/test_sprint2_market_alert_feedback.py @@ -5,7 +5,7 @@ import pytest from fastapi.testclient import TestClient import app.api.market as market_api -from app.advisor_db import AgentSessionLocal +from app import advisor_db from app.main import app from app.model.entities import AuditLog from app.model.entities_advisor import CopyTrackLog, MarketAlert @@ -94,7 +94,7 @@ def test_adopt_reuses_copy_gate_and_marks_alert_copied(): assert persisted.status == "copied" assert persisted.advisor_feedback == "adopt" - with AgentSessionLocal() as session: + with advisor_db.AgentSessionLocal() as session: track = session.query(CopyTrackLog).filter(CopyTrackLog.track_id == result.track_id).one() audit = ( session.query(AuditLog) diff --git a/tests/test_sprint2_market_alert_generation.py b/tests/test_sprint2_market_alert_generation.py index 0b54ae4..95ed592 100644 --- a/tests/test_sprint2_market_alert_generation.py +++ b/tests/test_sprint2_market_alert_generation.py @@ -16,12 +16,9 @@ from scripts.seed.import_compliance_rules import import_rules_from_markdown client = TestClient(app) def token_for(username: str, password: str) -> str: - response = client.post( - "/api/v1/auth/login", - json={"username": username, "password": password}, - ) - assert response.status_code == 200 - return response.json()["data"]["access_token"] + from tests.advisor_test_utils import login_token + + return login_token(username, password) class FakeGenerationLLM: def __init__(self, output: str | Exception) -> None: diff --git a/tests/test_sprint2_market_alert_scan.py b/tests/test_sprint2_market_alert_scan.py index bc06631..9c3baac 100644 --- a/tests/test_sprint2_market_alert_scan.py +++ b/tests/test_sprint2_market_alert_scan.py @@ -6,7 +6,7 @@ from sqlalchemy import inspect import app.api.market as market_api -from app.advisor_db import agent_engine +from app import advisor_db from app.main import app from tests.advisor_test_utils import login_staff_token, STAFF_COMPLIANCE from app.model.advisor_schemas import MarketFundQuote, MarketNavPoint @@ -54,7 +54,7 @@ class FakeMarketDataProvider: return self.quotes def test_market_alert_migration_creates_table(): - inspector = inspect(agent_engine) + inspector = inspect(advisor_db.agent_engine) assert "market_alert" in inspector.get_table_names() columns = {column["name"] for column in inspector.get_columns("market_alert")} diff --git a/tests/test_sprint2_market_data_provider.py b/tests/test_sprint2_market_data_provider.py index d6d9519..d297795 100644 --- a/tests/test_sprint2_market_data_provider.py +++ b/tests/test_sprint2_market_data_provider.py @@ -5,12 +5,9 @@ from app.main import app client = TestClient(app) def token_for(username: str, password: str) -> str: - response = client.post( - "/api/v1/auth/login", - json={"username": username, "password": password}, - ) - assert response.status_code == 200 - return response.json()["data"]["access_token"] + from tests.advisor_test_utils import login_token + + return login_token(username, password) def test_market_data_service_returns_latest_and_history_from_core_nav(): from app.service.market_data_service import MarketDataService diff --git a/tests/test_sprint2_template_hybrid_search.py b/tests/test_sprint2_template_hybrid_search.py index 0fd3996..5a011e4 100644 --- a/tests/test_sprint2_template_hybrid_search.py +++ b/tests/test_sprint2_template_hybrid_search.py @@ -4,7 +4,7 @@ from uuid import uuid4 from fastapi.testclient import TestClient import app.api.advisor_script_templates as templates_api -from app.advisor_db import AgentSessionLocal +from app import advisor_db from app.main import app from app.model.entities_advisor import ScriptTemplate from app.model.advisor_schemas import AuthContext @@ -25,12 +25,9 @@ def advisor_auth() -> AuthContext: ) def token_for(username: str, password: str) -> str: - response = client.post( - "/api/v1/auth/login", - json={"username": username, "password": password}, - ) - assert response.status_code == 200 - return response.json()["data"]["access_token"] + from tests.advisor_test_utils import login_token + + return login_token(username, password) def create_approved_template( *, @@ -41,7 +38,7 @@ def create_approved_template( created_by: str | None = None, ) -> ScriptTemplate: actor = created_by or f"test:hybrid_search:{uuid4().hex}" - with AgentSessionLocal() as session: + with advisor_db.AgentSessionLocal() as session: template = ScriptTemplate( scene=scene, customer_type="C3", diff --git a/tests/test_sprint2_template_import.py b/tests/test_sprint2_template_import.py index 8bea5c4..a73afe0 100644 --- a/tests/test_sprint2_template_import.py +++ b/tests/test_sprint2_template_import.py @@ -2,9 +2,10 @@ import subprocess import sys from pathlib import Path +import pytest from fastapi.testclient import TestClient -from app.advisor_db import AgentSessionLocal +from app import advisor_db from app.main import app from app.model.entities_advisor import ScriptTemplate @@ -14,15 +15,12 @@ SCRIPT = ROOT / "scripts" / "seed" / "import_script_templates.py" client = TestClient(app) def token_for(username: str, password: str) -> str: - response = client.post( - "/api/v1/auth/login", - json={"username": username, "password": password}, - ) - assert response.status_code == 200 - return response.json()["data"]["access_token"] + from tests.advisor_test_utils import login_token + + return login_token(username, password) def count_seed_templates() -> int: - with AgentSessionLocal() as session: + with advisor_db.AgentSessionLocal() as session: return session.query(ScriptTemplate).filter(ScriptTemplate.created_by == "seed:template_test_data").count() def test_template_dataset_loads_at_least_20_test_templates(): @@ -52,7 +50,7 @@ def test_template_import_is_idempotent_and_keeps_test_templates_unapproved(): assert second.total == first.total assert count_seed_templates() == first.total - with AgentSessionLocal() as session: + with advisor_db.AgentSessionLocal() as session: rows = session.query(ScriptTemplate).filter(ScriptTemplate.created_by == "seed:template_test_data").all() assert rows assert all(row.is_approved is False for row in rows) @@ -74,6 +72,7 @@ def test_advisor_search_does_not_return_unapproved_seed_templates(): assert response.status_code == 200 assert response.json()["data"]["items"] == [] +@pytest.mark.skip(reason="F-ε: 子进程跑 seed 脚本直连真实 demo 库,会翻转 script_template 审核态;in-process 用例已覆盖 import 行为") def test_template_import_script_prints_import_summary(): from scripts.seed.import_compliance_rules import import_rules_from_markdown diff --git a/tests/test_sprint2_template_library.py b/tests/test_sprint2_template_library.py index e3def2f..f9a135b 100644 --- a/tests/test_sprint2_template_library.py +++ b/tests/test_sprint2_template_library.py @@ -3,18 +3,15 @@ from uuid import uuid4 from fastapi.testclient import TestClient from sqlalchemy import inspect -from app.advisor_db import AgentSessionLocal, agent_engine +from app import advisor_db from app.main import app client = TestClient(app) def token_for(username: str, password: str) -> str: - response = client.post( - "/api/v1/auth/login", - json={"username": username, "password": password}, - ) - assert response.status_code == 200 - return response.json()["data"]["access_token"] + from tests.advisor_test_utils import login_token + + return login_token(username, password) def advisor_token() -> str: return token_for("advisor_test", "advisor_test") @@ -44,7 +41,7 @@ def create_template(headers: dict, payload: dict | None = None) -> dict: def latest_use_log(use_id: str): from app.model.entities_advisor import TemplateUseLog - with AgentSessionLocal() as session: + with advisor_db.AgentSessionLocal() as session: row = session.query(TemplateUseLog).filter(TemplateUseLog.use_id == use_id).one() session.expunge(row) return row @@ -52,14 +49,14 @@ def latest_use_log(use_id: str): def load_template(template_id: int): from app.model.entities_advisor import ScriptTemplate - with AgentSessionLocal() as session: + with advisor_db.AgentSessionLocal() as session: row = session.get(ScriptTemplate, template_id) assert row is not None session.expunge(row) return row def test_template_migration_creates_template_and_use_log_tables(): - inspector = inspect(agent_engine) + inspector = inspect(advisor_db.agent_engine) assert "script_template" in inspector.get_table_names() assert "template_use_log" in inspector.get_table_names() diff --git a/tests/test_sprint2_template_vectors.py b/tests/test_sprint2_template_vectors.py index eacf267..e173cda 100644 --- a/tests/test_sprint2_template_vectors.py +++ b/tests/test_sprint2_template_vectors.py @@ -5,7 +5,7 @@ from uuid import uuid4 from fastapi.testclient import TestClient -from app.advisor_db import AgentSessionLocal +from app import advisor_db from app.main import app from app.model.entities_advisor import ScriptTemplate from app.model.advisor_schemas import AuthContext, TemplateUpdate @@ -46,12 +46,9 @@ class FakeVectorStore: self.deleted_template_ids.append(template_id) def token_for(username: str, password: str) -> str: - response = client.post( - "/api/v1/auth/login", - json={"username": username, "password": password}, - ) - assert response.status_code == 200 - return response.json()["data"]["access_token"] + from tests.advisor_test_utils import login_token + + return login_token(username, password) def compliance_auth() -> AuthContext: token = token_for("compliance_test", "compliance_test") @@ -66,7 +63,7 @@ def create_template_row( created_by: str = "test:template_vectors", ) -> ScriptTemplate: suffix = uuid4().hex[:8] - with AgentSessionLocal() as session: + with advisor_db.AgentSessionLocal() as session: template = ScriptTemplate( scene="loss_comfort", customer_type="C3", @@ -88,7 +85,7 @@ def create_template_row( return template def load_embedding_id(template_id: int) -> str | None: - with AgentSessionLocal() as session: + with advisor_db.AgentSessionLocal() as session: template = session.get(ScriptTemplate, template_id) assert template is not None return template.embedding_id diff --git a/tests/test_sprint3_kyc_chat.py b/tests/test_sprint3_kyc_chat.py index 5d7690d..324db46 100644 --- a/tests/test_sprint3_kyc_chat.py +++ b/tests/test_sprint3_kyc_chat.py @@ -4,7 +4,7 @@ import pytest from fastapi.testclient import TestClient import app.api.kyc as kyc_api -from app.advisor_db import AgentSessionLocal, agent_engine +from app import advisor_db from app.main import app from app.model.entities import AgentMessage from app.model.advisor_schemas import AuthContext, KycChatRequest, KycSessionCreate @@ -116,7 +116,7 @@ def test_chat_parses_fields_updates_progress_and_persists_messages(): assert result.parser_degraded is False assert result.suggested_question - with AgentSessionLocal() as session: + with advisor_db.AgentSessionLocal() as session: messages = ( session.query(AgentMessage) .filter(AgentMessage.session_id == session_id) @@ -390,7 +390,7 @@ def test_complete_api_exposes_existing_session_lifecycle_operation(): def inspect_unique_constraints(table_name: str) -> set[tuple[str, ...]]: from sqlalchemy import inspect - inspector = inspect(agent_engine) + inspector = inspect(advisor_db.agent_engine) return { tuple(constraint["column_names"]) for constraint in inspector.get_unique_constraints(table_name) diff --git a/tests/test_sprint3_kyc_session.py b/tests/test_sprint3_kyc_session.py index 8450e3b..2d2ef05 100644 --- a/tests/test_sprint3_kyc_session.py +++ b/tests/test_sprint3_kyc_session.py @@ -6,7 +6,7 @@ from fastapi.testclient import TestClient from sqlalchemy import inspect import app.api.kyc as kyc_api -from app.advisor_db import AgentSessionLocal, agent_engine +from app import advisor_db from app.main import app from app.model.entities import AgentSession from app.model.entities_advisor import KycSession @@ -50,7 +50,7 @@ def session_service() -> KycSessionService: ) def test_kyc_session_migration_and_creation_associate_agent_session(): - inspector = inspect(agent_engine) + inspector = inspect(advisor_db.agent_engine) assert "kyc_session" in inspector.get_table_names() columns = {column["name"] for column in inspector.get_columns("kyc_session")} assert { @@ -84,7 +84,7 @@ def test_kyc_session_migration_and_creation_associate_agent_session(): assert result.suggested_question assert "年龄" in result.suggested_question - with AgentSessionLocal() as session: + with advisor_db.AgentSessionLocal() as session: agent_session = ( session.query(AgentSession) .filter(AgentSession.session_id == result.session_id) @@ -127,7 +127,7 @@ def test_expired_in_progress_session_is_archived_as_abandoned(): trace_id=f"trace-kyc-expire-create-{uuid4().hex}", ) old_time = datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(hours=5) - with AgentSessionLocal() as session: + with advisor_db.AgentSessionLocal() as session: row = session.query(KycSession).filter(KycSession.session_id == created.session_id).one() row.started_at = old_time row.updated_at = old_time