chore(sync): zsy_developcc 全量同步至 qyqy_develop(W26 口径)
- 分支内容对齐 qyqy_develop b6ec3aa,树完全一致(同步后 git diff 为空) - 覆盖本轮全部交付:客服 Agent 重构(安全路由 / 五出口 / 记忆与画像 / RAG 全链路) + 开发文档 62 份编号体系(D1.1 v1.17 索引) + 新增 D2.10-客服Agent端到端答辩文档-2026-09-21.html - 基线:e239eb7(2026-09-17 品牌口径统一快照),本提交为其直接后继
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
@@ -39,6 +37,7 @@ async def test_portal_root_redirects_to_public_home() -> None:
|
||||
"/portal/customer/orders/",
|
||||
"/portal/customer/transactions/",
|
||||
"/portal/customer/cash-ledger/",
|
||||
"/portal/customer/advisor-plans/",
|
||||
"/portal/customer/risk-questionnaire/",
|
||||
"/portal/employee-console/login/",
|
||||
"/portal/employee-console/workspace/",
|
||||
@@ -166,6 +165,71 @@ def test_api_client_registers_onboarding_risk_and_admin_endpoints() -> None:
|
||||
assert f"{endpoint_id}:" in source
|
||||
|
||||
|
||||
def test_advisor_workspace_registers_documented_operation_endpoints() -> None:
|
||||
source = (PORTAL / "common" / "api-client.js").read_text(encoding="utf-8")
|
||||
dashboard = (PORTAL / "employee-advisor" / "dashboard" / "index.html").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
for endpoint_id in (
|
||||
"ADVISOR_PUBLISHED", "ADVISOR_HISTORY", "ADVISOR_GOAL", "ADVISOR_ANALYSIS",
|
||||
"ADVISOR_ALLOCATION", "ADVISOR_RECOMMEND", "ADVISOR_CREATE_GOAL",
|
||||
"ADVISOR_REVIEW_RECOMMENDATION", "ADVISOR_PUBLISH_RECOMMENDATION",
|
||||
"ADVISOR_DELETE_RECOMMENDATION",
|
||||
):
|
||||
assert f"{endpoint_id}:" in source
|
||||
for label in ("组合分析", "资产配置", "生成推荐方案", "录入客户目标", "历史方案记录"):
|
||||
assert label in dashboard
|
||||
|
||||
|
||||
def test_advisor_plan_view_is_shared_by_both_surfaces() -> None:
|
||||
"""推荐方案的可视化渲染必须**只有一份**,投顾工作台与客户页共用。
|
||||
|
||||
两处都是同一份 `advisor_recommendation_plan`,各写一套必然漂移
|
||||
(改了一边忘了另一边)。这条测试同时守住「共享模块存在」与「两处都在用它」。
|
||||
"""
|
||||
view = (PORTAL / "common" / "advisor-plan-view.js").read_text(encoding="utf-8")
|
||||
assert "export function renderPlanProducts" in view
|
||||
assert "export async function hydratePlanView" in view
|
||||
assert "P002" in view
|
||||
for page in (
|
||||
"employee-advisor/dashboard/actions-module.js",
|
||||
"customer/advisor-plans/advisor-plans.js",
|
||||
):
|
||||
source = (PORTAL / page).read_text(encoding="utf-8")
|
||||
assert "advisor-plan-view.js" in source, page
|
||||
|
||||
|
||||
def test_advisor_history_module_exposes_plan_actions() -> None:
|
||||
"""历史方案记录里,推荐方案卡片要带审核/驳回/发送/删除四个操作。
|
||||
|
||||
四个动作对应三个端点(审核与驳回共用一个 reviews 端点,用 `decision` 区分)。
|
||||
少了 `ADVISOR_DELETE_RECOMMENDATION` 就只剩"能看不能删"。
|
||||
"""
|
||||
source = (
|
||||
PORTAL / "employee-advisor" / "dashboard" / "history-module.js"
|
||||
).read_text(encoding="utf-8")
|
||||
for label in ("审核通过", "驳回", "发送给客户", "删除"):
|
||||
assert label in source
|
||||
for endpoint in (
|
||||
"ADVISOR_REVIEW_RECOMMENDATION",
|
||||
"ADVISOR_PUBLISH_RECOMMENDATION",
|
||||
"ADVISOR_DELETE_RECOMMENDATION",
|
||||
):
|
||||
assert endpoint in source
|
||||
|
||||
|
||||
def test_advisor_dashboard_is_composed_from_feature_modules() -> None:
|
||||
source = (PORTAL / "employee-advisor" / "dashboard" / "dashboard.js").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert "./actions-module.js" in source
|
||||
assert "./history-module.js" in source
|
||||
config = (PORTAL / "employee-advisor" / "dashboard" / "advisor-config.js").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert "ACTION_LABELS" in config
|
||||
|
||||
|
||||
def test_no_portal_page_includes_the_same_script_twice() -> None:
|
||||
"""同一个入口 JS 被引两次(哪怕 `?v=` 不同)会让页面出现两份顶部导航。
|
||||
|
||||
@@ -413,3 +477,77 @@ def test_public_home_uses_a_local_hero_image() -> None:
|
||||
assert "/static/portal/guest/home/assets/wealth_architecture_hero.jpg" in home
|
||||
assert image.is_file()
|
||||
assert image.stat().st_size > 100_000
|
||||
|
||||
|
||||
def test_customer_service_widget_is_mounted_by_the_shell_for_public_and_customer_modes() -> None:
|
||||
"""浮窗由 `mountShell()` **单点挂载**,且只挂公开页与客户工作台。
|
||||
|
||||
挂载点选在 shell 而不是九个页面的 JS:写九份就意味着九处 `?v=` 版本号要一起改,
|
||||
漏一个就是"某个页面浮窗样式陈旧"。员工四类工作台(risk / advisor / operator / admin)
|
||||
刻意不挂 —— 它们各自有业务 Agent,挂上只会让"当前账号能不能用这个入口"变成
|
||||
一道需要解释的问题。
|
||||
"""
|
||||
shell = (PORTAL / "common" / "layout" / "app-shell.js").read_text(encoding="utf-8")
|
||||
widget = PORTAL / "common" / "customer-service-widget"
|
||||
assert (widget / "widget.js").is_file()
|
||||
assert (widget / "widget.css").is_file()
|
||||
import_line = (
|
||||
"import { mountCustomerServiceWidget } from "
|
||||
"'/static/portal/common/customer-service-widget/widget.js';"
|
||||
)
|
||||
assert import_line in shell
|
||||
assert "const CUSTOMER_SERVICE_MODES = Object.freeze(['public', 'customer']);" in shell
|
||||
assert "if (!CUSTOMER_SERVICE_MODES.includes(mode)) return;" in shell
|
||||
assert "mountCustomerServiceFor(mode);" in shell
|
||||
assert "'/static/portal/common/customer-service-widget/widget.css?v=" in shell
|
||||
|
||||
|
||||
def test_customer_service_widget_reuses_shared_visitor_token_and_endpoint_table() -> None:
|
||||
"""浮窗不得自带第二份访客令牌实现,也不得绕过端点表直接 `fetch`。"""
|
||||
source = (
|
||||
PORTAL / "common" / "customer-service-widget" / "widget.js"
|
||||
).read_text(encoding="utf-8")
|
||||
assert "common/visitor-token.js" in source
|
||||
assert "common/api-client.js" in source
|
||||
assert "visitorHeaders()" in source
|
||||
# 访客令牌只应有 `visitor-token.js` 一份实现:存储 key 与 JWT 解析都不该出现在这里。
|
||||
assert "portalVisitorToken" not in source
|
||||
assert "sessionStorage" not in source
|
||||
assert "atob(" not in source
|
||||
# 所有请求走端点表(`test_business_pages_do_not_call_fetch_directly` 的同一口径)。
|
||||
assert "fetch(" not in source
|
||||
for endpoint_id in ("'C001'", "'R001'", "'R002'", "'C005'"):
|
||||
assert endpoint_id in source, endpoint_id
|
||||
# 轮询预算要覆盖 Worker 的整条链路(实测 4.1–4.8 秒),不得退回"几次就放弃"。
|
||||
assert "const POLL_ATTEMPTS = 40;" in source
|
||||
|
||||
|
||||
def test_customer_service_widget_does_not_show_tool_names_as_sources() -> None:
|
||||
"""`result.source_references` 目前只有 `tool` 类型(标题就是工具名)。
|
||||
|
||||
知识来源引用是 `C-10` 乙的**降级项**:治理层不认可 `knowledge` 来源,一旦输出会让
|
||||
整个 run 失败。所以浮窗**刻意不渲染「参考:」行** —— 显示「参考:query_knowledge」
|
||||
对客户毫无意义。可追溯性由审计承接。此断言防止有人"顺手"把它加回来。
|
||||
"""
|
||||
source = (
|
||||
PORTAL / "common" / "customer-service-widget" / "widget.js"
|
||||
).read_text(encoding="utf-8")
|
||||
css = (
|
||||
PORTAL / "common" / "customer-service-widget" / "widget.css"
|
||||
).read_text(encoding="utf-8")
|
||||
assert "snapshot.result?.source_references" not in source
|
||||
assert "addReferences" not in source
|
||||
assert "cs-widget__references" not in source
|
||||
assert "cs-widget__references" not in css
|
||||
assert "C-10" in source # 降级理由留痕,不能只删代码不写原因
|
||||
|
||||
|
||||
def test_api_client_lets_callers_pin_an_explicit_bearer_token() -> None:
|
||||
"""调用方显式传入的 `Authorization` 优先于自动附加的登录令牌。
|
||||
|
||||
否则"带访客令牌取公开数据"会在浏览器恰好有登录令牌时静默变成"用登录身份取数据",
|
||||
症状是同一个公开页对访客与已登录用户显示不同内容(`README.md` 明令禁止混用)。
|
||||
"""
|
||||
source = (PORTAL / "common" / "api-client.js").read_text(encoding="utf-8")
|
||||
assert "const callerAuth = options.headers?.Authorization;" in source
|
||||
assert "if (endpoint.auth !== false && token && !callerAuth) {" in source
|
||||
|
||||
Reference in New Issue
Block a user