test-spec.md 20 KB

单测 Spec — Spring AI Alibaba 供应商适配层与 DashScope 原生接入

status: done created: 2026-07-10

0. 测试原则

  • Red/Green TDD:Adapter 路由、参数映射、密钥保护和缓存失效必须先有失败测试,再实现最小代码使其通过;
  • First Run the Tests:开始编码前先跑 AiInvocationResolverTest,记录当前测试工具链和基线;
  • 展示工作:所有命令、关键输出、失败原因和跳过项追加到 execution-log.md,禁止只写“测试通过”;
  • 增量复用:后续 /test 先读取本文件、execution-log.mdspec.mdtasks.md,按 automated-testing-standard.md 追加本轮差异;
  • 无真实密钥:单元测试使用格式明显的假值,真实 DashScope 仅作为人工可选集成验证;
  • 不污染环境:只停止本轮启动的服务,不清理用户已有 Maven、数据库、前端或其他进程。

1. 测试框架

项目
JUnit 版本 JUnit Jupiter 5(由 Spring Boot 3.5.13spring-boot-starter-test 管理)
Mock 框架 Mockito + MockitoExtension
断言 JUnit Assertions;需要类型/集合表达时可使用 AssertJ
已有测试数量 forge-plugin-ai 当前 1 个测试类、2 个测试方法:AiInvocationResolverTest
已有测试风格 纯单元测试,Mock Service,直接构造被测对象,不启动 Spring Context
默认测试开关 根 POM 默认跳过测试,执行时必须启用 -Penable-tests

2. 覆盖范围

P0 — 核心业务逻辑(必须覆盖)

类名: AiProviderAdapterCode

方法 场景 输入 预期结果
require Compatible 合法值 openai_compatible 返回对应枚举
require DashScope 合法值 dashscope_native 返回对应枚举
require 非法值 null""、纯空白、unknown BusinessException,不回退默认 Adapter

类名: AiProviderAdapterRegistry

方法 场景 输入 Mock 行为 预期结果
构造器 正常注册 两个不同 code Adapter 返回各自 code Registry 创建成功
构造器 重复注册 两个相同 code Adapter 返回相同 code 启动期失败,错误指出重复 code
getRequired 命中 dashscope_native Registry 含对应 Adapter 返回 DashScope Adapter
getRequired 未命中 unknown Registry 无对应 Adapter BusinessException,不调用其他 Adapter
createChatModel 合法配置 provider + options Adapter validate 通过 按顺序执行 getRequired、validate、create,返回模型
createChatModel 校验失败 错误 URL/API Key/model Adapter validate 抛错 不调用 Adapter#createChatModel,不产生网络请求

类名: AiProviderBaseUrlPolicy

方法 场景 输入 预期结果
normalizeAndValidate URI 安全校验 非 HTTP scheme、query、fragment、userInfo 明确业务异常
normalizeAndValidate Native 官方根地址 https://dashscope.aliyuncs.com/ 归一化为无尾斜杠根地址
normalizeAndValidate Native 误用 Compatible 官方域名 /compatible-mode 拒绝
normalizeAndValidate Compatible 官方地址 官方域名 /compatible-mode/ 接受并归一化尾斜杠
normalizeAndValidate Compatible 误用 Native 官方域名根地址 拒绝
normalizeAndValidate Compatible 错误版本路径 官方域名 /compatible-mode/v1 拒绝,避免 SDK 再追加 /v1
normalizeAndValidate 自定义代理 合法 HTTP/HTTPS 自定义域名 仅做通用校验,不追加或替换路径

类名: OpenAiCompatibleProviderAdapter

方法 场景 输入 预期结果
validate 完整配置 API Key、Base URL、model 通过
validate Base URL 缺失 空 Base URL 明确业务异常
validate 阿里官方原生根地址误配 https://dashscope.aliyuncs.com 明确业务异常,不发请求
createChatModel 参数映射 model、temperature、maxTokens 返回 OpenAiChatModel,默认 Options 值一致

类名: DashScopeNativeProviderAdapter

方法 场景 输入 预期结果
validate 官方原生地址 https://dashscope.aliyuncs.com 通过
validate Base URL 为空 空值 使用官方默认地址
validate Compatible 地址误配 包含 /compatible-mode 明确业务异常,不发请求
createChatModel 参数映射 qwen-plus、0.7、512 返回 DashScopeChatModel;Options 为 DashScopeChatOptionsmaxTokens=512
createChatModel Tool Calling 基础类型 默认 Options Options 实现 ToolCallingChatOptions

类名: AiProviderSecretMasker

方法 场景 输入 预期结果
mask 长密钥 abcd12345678wxyz abcd****wxyz
mask 短密钥 长度小于等于 8 固定掩码,不泄露字符
mask 空值 null/blank 空安全结果
isUnchangedMask 未修改 提交值等于持久值的 mask true
isUnchangedMask 新密钥 提交新的明文 false

类名: AiProviderService

方法 场景 输入 Mock 行为 预期结果
updateProvider 脱敏值回写 ID + masked key Mapper 返回原实体 保留原密钥;成功后 evict
updateProvider 替换密钥 ID + 新明文 Mapper 返回原实体 保存新密钥;成功后 evict
createProvider Adapter 未提供/null adapterCode=null 写入 openai_compatible
updateProvider 旧页面未传 Adapter ID + adapterCode=null Mapper 返回原生 Adapter 实体 保留原 dashscope_native,不得回退 Compatible
createProvider/updateProvider 显式 blank Adapter adapterCode="" 或纯空白 业务校验失败,不写数据库
updateProvider DB 更新失败 合法请求 update 返回 false/抛错 抛异常;不得把失败写成成功
testConnection 已保存供应商 仅 ID 按租户加载含真实 key 的实体;Adapter 返回 Mock ChatModel Prompt 通过指定 Adapter 调用;不依赖浏览器 key
testConnection 未保存配置 无 ID + 完整配置 Registry 返回 Mock ChatModel 可测试;密钥不进入日志/结果
testConnection one-of 混合请求 ID + 任一 adapter/baseUrl/apiKey/model 字段 请求校验失败,不加载或调用模型
testConnection 敏感 SDK 异常 无 ID 完整配置 ChatModel 抛出包含假密钥的异常 返回固定安全错误,不包含假密钥、Header 或请求体
toSafeView 列表/详情 含真实 key 实体 VO 只含脱敏 key
deleteProvider 删除成功 providerId 无关联模型、remove 成功 持久化成功后调度 after-commit evict

类名: ChatClientCache

方法 场景 输入 Mock 行为 预期结果
getOrCreateBase 相同配置 同 provider 实体/adapter/options Registry 每次可创建模型 实际只创建一次
getOrCreateBase 租户不同 两个实体 tenantId 不同、providerId 相同 Registry 创建模型 两个缓存项,不串租户
getOrCreateBase Adapter 不同 同 providerId、不同 adapter Registry 创建模型 两个缓存项
evictByProvider 配置更新 tenantId + providerId 已有缓存 清除所有模型/参数组合;下次重新创建

类名: AiProviderCacheEvictionScheduler

方法 场景 输入 预期结果
scheduleAfterCommit 活动事务未提交 provider 实体 提交前不调用 Cache
scheduleAfterCommit 事务提交 provider 实体 afterCommit 只清理实体 tenantId/providerId
scheduleAfterCommit 事务回滚 provider 实体 不清理 Cache
scheduleAfterCommit 无活动事务 provider 实体 立即清理
scheduleAfterCommit 实际事务已激活但同步未启用 provider 实体 BusinessException,不提前清理 Cache

类名: AiClientImpl

方法 场景 输入 Mock 行为 预期结果
call Native 供应商同步调用 Resolver 返回 dashscope_native provider 真实 Cache 经 Mock Registry 获得 Fake ChatModel 统一 ChatClient 链路返回内容,Registry 收到 Native provider 和通用 Options
stream Native 供应商流式调用 Resolver 返回 dashscope_native provider Fake ChatModel 依次返回 reasoning/content ChatResponse 输出包含思考和回答,完整内容按现有规则持久化
stream reasoningContent AssistantMessage metadata 含该键 无公网调用 现有解析逻辑读取推理内容,不依赖 OpenAI 类型

类名: AiInvocationResolver

保留现有两个用例,并增加:DashScope Native 的 Base URL 可由 Adapter 默认,不在 Resolver 统一拒绝;API Key 缺失仍在 Adapter 校验阶段失败。

2026-07-11 默认模型与错误诊断增量

类名 场景 预期结果
AiModelService 存在启用且默认模型 返回 Mapper XML 查询到的 model_id
AiModelService 无启用默认模型 BusinessException("请为供应商设置默认模型")
AiProviderService 已保存供应商的双写默认字段过期 忽略 ai_provider.default_model,使用 ai_model 权威结果
AiProviderService 已保存供应商无权威默认模型 在 Registry/ChatModel 调用前失败,不产生网络请求
AiProviderService 未保存供应商测试 使用请求内 defaultModel,不查询 ai_model
AiInvocationResolver 无显式模型且 Agent 无模型 使用 ai_model 权威默认模型,不读取供应商双写字段
AiInvocationResolver 无权威默认模型 明确提示配置默认模型,不回退 gpt-3.5-turbo
AiProviderFailureDiagnostics 400 - {error.code/type} 仅提取 httpStatus=400 与白名单化错误码
AiProviderFailureDiagnostics 异常含密钥/任意正文 诊断对象和日志字段不包含原始 message、密钥或响应正文

P1 — 数据与迁移

对象 场景 验证
V1.0.17 首次执行 ai_provider.adapter_code 存在、非空、默认 Compatible
V1.0.17 历史回填 现有记录全部为 openai_compatible
V1.0.17 部分部署/重跑 预置一条 dashscope_native 后重跑保护逻辑
V1.0.17 字典写入 tenant_id=1,类型及两条数据存在且不重复
V1.0.17 防重复静态检查 字段检查使用 information_schema,字典使用 NOT EXISTS
Flyway placeholder 所有迁移 rg -n '\$\{[^}]+\}' forge-server/db/migration 无输出

P2 — 入口层和前端

场景 操作 预期结果
供应商分页 GET /ai/provider/page 包含 adapterCode,API Key 只有脱敏值
供应商详情 GET /ai/provider/{id} 不返回真实 API Key
旧供应商更新 不修改密钥直接保存 数据库 API Key 保持原值,缓存失效
新建 Compatible adapterCode=openai_compatible 保存成功,既有模型调用正常
新建 Native adapterCode=dashscope_native + 原生 URL 保存成功,可选择 qwen 模型
错误 Native URL adapterCode=dashscope_native + compatible-mode 保存或测试返回明确错误
错误 Compatible URL adapterCode=openai_compatible + DashScope 官方根地址 保存或测试返回明确错误
已保存连接测试 点击“测试连接” 加密 Network Payload 只有 ID,不含 API Key
混合连接测试 ID 与配置字段同时提交 后端拒绝,不加载数据库密钥后再接受请求覆盖
敏感写请求 新增、更新、测试 POST 使用 postEncrypt,PUT 带 encrypt:true,后端保持 @ApiDecrypt
前端构建 Node 20.19.0 + pnpm build 构建成功,无字典或模板错误

2026-07-11 管理端增量

场景 操作 预期结果
供应商分页 切换每页条数/快速跳页 请求携带响应式 pageNum/pageSize,总数来自 itemCount
模型分页 供应商模型超过一页 服务端分页,无 pageSize=100 静默截断,数量显示总条数
搜索重置 筛选后点击重置 清空筛选、页码、当前供应商与模型工作区
行选择 重复点击已选中供应商 保持选中;仅显式关闭按钮收起详情
键盘选择 聚焦供应商行后 Enter/Space 选择供应商并加载模型
上传过滤 打开文件选择器 accept=.png,.jpg,.jpeg,.svg,.webp 生效
API Key 编辑 编辑已保存供应商 展示脱敏值且明确“留空表示不修改”,提交不还原密钥
模型默认 点击“设为默认” 更新模型默认状态并刷新供应商权威默认模型
连接测试结果 成功或失败 图标、标题、说明共同表达状态,非纯颜色反馈
响应式与主题 375/768/1024/1440、明暗色 无页面级横向溢出,边界和文字对比清晰

P3 — 可选真实集成

仅当人工提供 AI_DASHSCOPE_API_KEY、网络可用且明确接受少量模型调用成本时执行:

  1. qwen-plus 同步请求返回可见文本;
  2. 流式请求持续输出且正常结束;
  3. 推理模型返回 reasoningContent 时现有解析逻辑可读取;
  4. 连接测试 maxTokens 受限;
  5. 日志、异常和响应均不出现 API Key。

不测试(明确列出原因)

  • Agent Framework、Graph、MCP、Nacos、Studio:不在本变更依赖和功能范围;
  • Embedding/Image/Audio/Rerank:本阶段仅实现 Chat Adapter;
  • API Key 数据库加密/KMS:属于独立安全变更;
  • 公网真实模型作为 CI 必跑项:依赖外部网络、凭据和费用,不具备可重复性;
  • 未绑定菜单的 forge-admin-ui/src/views/ai/provider.vue:后端“新增缺失则 Compatible、更新缺失则保留原 Adapter”保证旧请求兼容,本次只验证实际菜单页面。

3. 执行计划

  • Step 1: 运行 AiInvocationResolverTest 和目标模块 dependency tree,确认基线;
  • Step 2: 按 Task 2-5 逐类生成 P0 测试,并强制包含 AiClientImplTest 同步/流式用例,保存 Red 输出;
  • Step 3: 实现最小代码并保存 Green 输出;
  • Step 4: 运行 AI 插件完整测试、编译和主应用装配;
  • Step 5: 执行 Flyway 静态检查,有 dev 库时实跑 V1.0.17;
  • Step 6: 使用 Node v20.19.0 执行前端 build;
  • Step 7: 按环境条件决定是否执行真实 DashScope 集成验证并记录跳过原因;
  • Step 8: 回填执行证据、警告、服务清理和 Spec/Task 状态。

3.1 建议命令

cd forge-server
mvn -pl forge-framework/forge-plugin-parent/forge-plugin-ai dependency:tree \
  '-Dincludes=org.springframework.ai:*,com.alibaba.cloud.ai:*' -Dverbose
cd forge-server
mvn -Penable-tests \
  -pl forge-framework/forge-plugin-parent/forge-plugin-ai -am test \
  -Dtest=AiInvocationResolverTest,AiClientImplTest,AiProviderAdapterCodeTest,AiProviderAdapterRegistryTest,AiProviderBaseUrlPolicyTest,OpenAiCompatibleProviderAdapterTest,DashScopeNativeProviderAdapterTest,AiProviderSecretMaskerTest,AiProviderCacheEvictionSchedulerTest,AiProviderServiceTest,ChatClientCacheTest \
  -Dsurefire.failIfNoSpecifiedTests=false
cd forge-server
mvn -pl forge-framework/forge-plugin-parent/forge-plugin-ai -am compile -DskipTests
cd forge-server
mvn -pl forge-admin-server -am package -DskipTests
rg -n '\$\{[^}]+\}' forge-server/db/migration
source ~/.nvm/nvm.sh
nvm use v20.19.0
cd forge-admin-ui
NODE_OPTIONS=--max-old-space-size=8192 pnpm build

4. 历史验证基线

时间 范围 命令 结果 备注
2026-07-10 Research 未执行构建 /apply 前建立 当前仅创建提案;禁止把静态阅读记为测试通过
2026-07-10 Apply 基线 AiInvocationResolverTest 2 tests,0 failure/error/skip Java 17,-Penable-tests 确认测试真实执行

5. 本轮增量验证

时间 变更范围 必跑项 实际命令 结果 跳过/警告
2026-07-10 Spec/Tasks/Test Spec 文档 占位符/尾随空格、--no-index --check、路径/状态一致性、独立读者复审 文档静态检查通过;第三轮 Reader Test PASS 本轮仅文档,不编译业务代码 无服务启动
2026-07-10 Task 1 依赖基线 变更前后 dependency tree、AI 插件 compile Spring AI 全部为 1.1.2;DashScope Core 为 1.1.2.3;compile SUCCESS 无 DashScope Starter;无服务启动
2026-07-10 Task 2-5 P0 增量 Adapter Code/Registry/URL Policy/双 Adapter/Cache/Resolver/Secret/Service/Scheduler 各 Task Red/Green 证据见 execution-log.md;最终并入 AI 插件全量 44 tests JVM CDS 与 commons-logging 警告,不影响测试结论
2026-07-10 Task 7 Native 离线调用 AiClientImplTest 同步、流式、reasoningContent 和持久化 2 tests,0 failure/error/skip;真实 Cache 经 Mock Registry/Fake ChatModel 进入统一链路 首轮异步落库断言存在竞态,改用 timeout 等待后复跑通过
2026-07-10 Task 7 全量验收 AI 全量测试、AI package、Admin package、Flyway 静态检查、Node 20 前端 build 44 tests 全过;24/35 模块 reactor package 成功;前端 8485 modules、build SUCCESS 无环境 API Key,公网验证跳过;未启动服务;既有编译/Vite 警告已记录
2026-07-10 Review 修复复验 事务同步异常 fail-closed、AI 插件完整测试 新增 Scheduler 边缘用例;45 tests,Failures/Errors/Skipped 均为 0;24 模块 reactor BUILD SUCCESS JVM CDS 与 commons-logging 既有警告;未启动服务
2026-07-11 /review 独立复审 origin/main..HEAD 真实代码、AI 完整测试、依赖树、前端构建、XML/Flyway/diff 静态检查 Spec Compliance PASS;Code Quality PASS;45 tests、24 模块 reactor、8485 modules 前端构建全部成功 未提供公网 API Key/隔离 dev 库;保留既有 JVM、commons-logging 与 Vite 警告;未启动服务
2026-07-11 默认模型与供应商页面增量 4 类后端定向测试、AI 插件完整测试、Mapper XML、目标 Vue ESLint、Node 20 build、Playwright 响应式与交互 定向 21 tests、AI 全量 51 tests 全过;前端 8485 modules build;1440/1024/375 无页面横向溢出,重复点击保持选中,控制台无错误 未再次调用真实供应商模型;复用用户现有 8580 服务;Playwright 启动的 5173 Vite 已停止

6. 执行证据

  • execution-log.mdcode-copilot/changes/archive/2026-07-11-spring-ai-alibaba-provider-adapter/execution-log.md
  • 关键接口:/ai/provider/page/ai/provider/{id}/ai/provider/ai/provider/testAiClient.call/stream
  • 关键数据库检查:ai_provider.adapter_codesys_dict_type.ai_provider_adapter_type、对应 sys_dict_dataforge_schema_history V1.0.17
  • 服务启动与停止:本变更未启动 forge-admin-server,无 PID 清理项
  • 公网集成凭据:只从环境变量读取,不写入命令日志、Spec、测试源码或 SQL

7. 归档验收

  • 状态:done
  • 归档时间:2026-07-11
  • 复用基线:后端定向 21 tests、AI 插件完整 51 tests、Admin 35 模块 package、Node 20 前端生产构建和 1440/1024/375 三视口页面验证均已有成功证据。
  • 本轮未重跑:用户明确要求自行验收最后一轮页面间距、模型列表拆列和默认模型行内开关微调,因此未再次执行前端构建或浏览器测试,也未把中断构建写成通过。
  • 环境跳过:未提供 AI_DASHSCOPE_API_KEY 和隔离 dev 数据库,公网 DashScope 与 Flyway 实库验证继续跳过。