ANALYSIS.md 17 KB

FWH MCP Server 测试 SKIP / FAIL 详细分析

配套 report.md(自动生成的用例明细)与 pii_report.md(脱敏专项)。 本文对 1 个 FAIL 和 73 个 SKIP 逐类给出根因分析、代码定位与修复建议。 分析基于源码阅读,非推测;关键行号已标注。


一、总览

类别 数量 是否缺陷 结论
FAIL 1 是(真实缺陷) service_feedback_history nil 指针 panic
SKIP-A 需真实样本参数 51 否 工具有必填参数,本轮无样本,未构造调用
SKIP-B 依赖未初始化 约 8 是(配置缺陷) MCP main.go 少调了 FWH / Plane / Queue 的初始化
SKIP-C 数据为空 3 否 生产库当日无对应统计文档
SKIP-D Plane 参数缺失被远端拒 3 次要(健壮性) handler 忽略必填校验错误,导致 404 而非清晰报错

核心结论:真正需要修的是 MCP Server 的初始化不完整(cmd/mcp/main.go)。它只初始化了 DB/Cache/MinIO/Plane 底层 client,漏掉了 3 处初始化,直接导致 1 个 FAIL + 约 8 个本应 PASS 的 SKIP。


二、FAIL 详细分析

FAIL-1 service_feedback_history — nil 指针 panic

现象

internal error: panic recovered in service_feedback_history tool handler:
runtime error: invalid memory address or nil pointer dereference

(panic 被 server.WithRecovery() 捕获,进程未崩溃,但该工具完全不可用。)

根因(已定位)

调用链:handleFeedbackHistory → services.GetFeedbackHistory("") → repositories.Plane.ListIntakeIssues(...)。

repositories.Plane 是一个包级全局变量,只有 repositories.InitPlane() 才会给它赋值:

repositories/repositories.go:22,26-29

var Plane *plane_v2.FeedbackPlaneService

// InitPlane 初始化新架构 Plane(启动时调用)
func InitPlane(cfg *config.PlaneWebApiConfig) {
    plane_v2.InitPlaneClient(cfg.Host, cfg.Email, cfg.Password)
    Plane = plane_v2.NewFeedbackPlaneService(cfg.WorkspaceSlug, cfg.ProjectId, ...)
}

// 通用初始化接口(给MCP Server等外部模块使用)
var InitPlaneClient = plane_v2.InitPlaneClient   // 只初始化底层 client,不赋值 Plane

而 MCP 的 cmd/mcp/main.go:48 只调用了 底层 的 InitPlaneClient:

repositories.InitPlaneClient(planeCfg.Host, planeCfg.Email, planeCfg.Password)

没有调用 repositories.InitPlane(...),所以 repositories.Plane 始终为 nil → 空指针解引用 panic。

修复建议(二选一)

方案 A(推荐,最小改动)——在 cmd/mcp/main.go 用 InitPlane 替换 InitPlaneClient:

// 原:repositories.InitPlaneClient(planeCfg.Host, planeCfg.Email, planeCfg.Password)
repositories.InitPlane(&cfg.PlaneWebApi)

InitPlane 内部已包含 InitPlaneClient,同时会给 repositories.Plane 赋值,data_plane_v2_* 与 service_feedback_* 都能正常工作。

方案 B(防御性,二者叠加)——保留底层 init 的同时补充:

repositories.InitPlaneClient(planeCfg.Host, planeCfg.Email, planeCfg.Password)
repositories.InitPlane(&cfg.PlaneWebApi)   // 追加,给 Plane 全局赋值

附加健壮性建议:services.GetFeedbackHistory 入口应做 nil 校验,避免未初始化时 panic:

func GetFeedbackHistory(userName string) ([]FeedbackHistoryItem, error) {
    if repositories.Plane == nil {
        return nil, fmt.Errorf("Plane 未初始化")
    }
    ...
}

三、SKIP 详细分析(按类)

SKIP-B 依赖未初始化(建议修复)

这些工具连接失败/URL 非法,根因都是 MCP main.go 缺少初始化,而非依赖服务本身不可用。

B-1 FWH 远程 API:http: no Host in request URL

受影响用例:data_fwh_query_site_base_info_list、data_fwh_query_site_list、 data_fwh_query_alarm_statistics、data_fwh_query_fault_record_call_record_list、 data_fwh_query_tou_template_list、service_tou_template_list 等(凡走 FWH Pro API)。

现象

Post "https:///hes-gateway/manage/selectIotTouTemplateList": http: no Host in request URL

注意 https:// 后面 host 为空。

根因(已定位)

FWH 客户端的 host 来自包级变量 fwhEnergy,只有 FwhHttpInit 会赋值:

repositories/api/fwh_pro/fwhHttpClient.go:16-31

var fwhEnergy string   // 未初始化时为 ""

func FwhHttpInit(cfg *config.FwhApiConfig) {
    fwhEnergy = cfg.FwhEnergy   // ← 关键赋值
    ...
}
func getFullEnergyUrl(relativeURL string) string {
    return "https://" + fwhEnergy + relativeURL   // fwhEnergy="" → "https:///..."
}

HTTP Server 的 main.go:61 有调用 repositories.FwhHttpInit(&cfg.FwhApi), 但 MCP 的 cmd/mcp/main.go 没有调用,因此 fwhEnergy 为空 → URL 无 host。

说明:config.env 里 FWH_ENERGY="energy.franklinwh.com" 是配置好的, 问题不在配置文件,而在 MCP 启动时没把它注入 FWH 客户端。

修复建议:在 cmd/mcp/main.go 初始化段补一行:

repositories.FwhHttpInit(&cfg.FwhApi)

(若还需测试环境 API,可按需追加 FwhTestApi。)

B-2 后台任务:inspector not initialized

受影响用例:service_background_task_list

根因(已定位)

task/queue/fwhQueue/fwhTaskQueue.go:98

return nil, fmt.Errorf("inspector not initialized")

Asynq 的 inspector 需在启动时初始化。MCP main.go 未初始化任务队列 inspector。

修复建议(按需):

  • 若希望 MCP 支持查询后台任务,在 main.go 补充任务队列 inspector 的初始化(参考 HTTP Server 启动流程中的队列初始化)。
  • 若 MCP 场景不需要该能力,建议 handler 在 inspector 为空时返回明确的业务提示(如"后台任务查询在当前部署未启用"),而非底层错误串,避免误导。

小结(SKIP-B 一次性修复)

在 cmd/mcp/main.go 的初始化段(现为 database.Init → cache.Init → minioUtil.InitClient → InitPlaneClient)补齐:

repositories.FwhHttpInit(&cfg.FwhApi)   // 修复 B-1:FWH API host
repositories.InitPlane(&cfg.PlaneWebApi) // 修复 FAIL-1 + Plane 相关
// (可选)初始化任务队列 inspector,修复 B-2

预计可将 FWH 相关 4~6 个 SKIP、feedback panic、以及后台任务查询一并转为可用。


SKIP-D Plane 参数被远端拒绝(次要健壮性问题)

受影响用例:plane_list_labels、plane_list_states(测试以 {} 调用),返回 404 Not Found。

根因(已定位)

这两个工具在 schema 中把 workspace_slug/project_id 声明为 Required, 但 handler 忽略了必填校验错误:

cmd/mcp/tools/data/plane_v2_api_handlers.go:164-177

func handlePlaneV2ListLabels(...) {
    slug, _ := req.RequireString("workspace_slug")   // ← 错误被丢弃
    projectId, _ := req.RequireString("project_id")  // ← 错误被丢弃
    data, err := planeV2Svc.ListLabels(slug, projectId) // 用空串请求 → 远端 404
    ...
}

对比正确写法(如 handlePlaneV2GetWorkspace / handlePlaneV2ListProjects)是检查错误并 return helper.ParamError(...)。

影响:功能上这些工具需要真实 slug/project_id 才能用(属 SKIP-A 类), 但由于未做前置校验,缺参时返回的是含糊的远端 404,而不是清晰的"参数 workspace_slug 必填"。

修复建议:统一补齐必填校验:

slug, err := req.RequireString("workspace_slug")
if err != nil { return helper.ParamError("workspace_slug"), nil }
projectId, err := req.RequireString("project_id")
if err != nil { return helper.ParamError("project_id"), nil }

建议全量排查 plane_v2_api_handlers.go 中所有 := req.RequireString(...) 后紧跟 , _ 丢弃错误的写法。

备注:plane_list_projects 用例本轮同样因缺 workspace_slug 被判 SKIP, 但它 handler 有正确校验,返回了清晰的"参数 workspace_slug 必填",属正常行为。


SKIP-A 需真实样本参数(非缺陷,测试限制)

受影响用例(约 51 个):所有含必填业务参数的只读工具,例如 service_site_query_info(sn)、service_sim_query(giccid)、 data_pgsql_query_snapshot_data(sn,date)、data_sim_*(iccid)、 plane_get_*(workspace_slug,project_id,...)、storage_minio_read_object(bucket,key) 等。

原因:这些工具需要真实存在的 SN / GICCID / 任务 ID / 文件名 / Plane UUID 才能构造有意义的调用;本轮测试无这些样本,驱动主动 SKIP(避免用假值产生无意义的错误)。

这不是缺陷。补测方法:提供样本后运行驱动的样本模式(见"如何补测")。这些工具的参数校验分支已在 L1 层抽样验证正确(缺参返回 参数 xx 必填)。


SKIP-C 数据为空(非缺陷)

受影响用例:service_monitor_alarm_stats、service_dashboard_alarm_detail(mongo: no documents in result)。

原因:生产库 StatisticsAlarm 集合当日/最新无文档。工具行为正确(查不到返回错误/空),只是恰好无数据。 service_monitor_shutdown_stats、service_dashboard_shutdown_detail 同类工具有数据、已 PASS,可佐证逻辑正常。

建议(可选):对"无文档"场景,services 层可返回空结果 + 提示(helper.EmptyResult)而非 error,语义更友好;当前作为 error 也可接受。


四、修复优先级建议

优先级 问题 修复位置 预期收益
P0 FAIL-1 feedback panic cmd/mcp/main.go 改用 InitPlane 消除 panic,Plane/feedback 可用
P0 B-1 FWH host 为空 cmd/mcp/main.go 补 FwhHttpInit 恢复所有 data_fwh_* / TOU 远程查询
P1 SKIP-D Plane handler 忽略必填校验 plane_v2_api_handlers.go 补错误检查 缺参时清晰报错而非 404
P2 B-2 后台任务 inspector cmd/mcp/main.go 补队列初始化 或 友好提示 后台任务查询可用/语义清晰
P2 GetFeedbackHistory nil 防御 services/feedbackServices.go 纵深防御
P3 SKIP-C 空数据语义 services 统计相关 语义友好(可选)

一次性关键修复(cmd/mcp/main.go,覆盖 P0):

// 现有:
database.Init(&cfg.App)
cache.Init(cfg.App.RedisAddr)
minioUtil.InitClient(&cfg.App)
repositories.InitPlaneClient(planeCfg.Host, planeCfg.Email, planeCfg.Password)

// 建议调整为:
database.Init(&cfg.App)
cache.Init(cfg.App.RedisAddr)
minioUtil.InitClient(&cfg.App)
repositories.FwhHttpInit(&cfg.FwhApi)      // 新增:FWH API host
repositories.InitPlane(&cfg.PlaneWebApi)   // 替换:同时初始化 Plane 全局实例

五、如何补测(回归验证)

  1. 应用上述 main.go 修复并重新构建:go build -o fwh-mcp-server ./cmd/mcp/
  2. 重新运行驱动: go run ./cmd/mcp/test/runner -bin <新二进制> -cwd /var/www/fwh-toolbox/server -out ./cmd/mcp/test
  3. 预期:FAIL-1 转 PASS;FWH/TOU 相关 SKIP-B 转 PASS 或 SKIP-A(若仍缺样本)。
  4. 提供真实 SN / GICCID / Plane workspace_slug+project_id / 已知 MinIO 对象后, 可为驱动增加样本入参(runL2 的 preset 段),将 SKIP-A 用例转为真实冒烟。

六、回归验证结果(已应用修复后)

已应用 P0 + P1 修复并重跑驱动,结果对比:

指标 修复前 修复后
PASS 50 57
FAIL 1 0
SKIP 73 67

已应用的修复:

  1. cmd/mcp/main.go:新增 repositories.FwhHttpInit(&cfg.FwhApi);将 InitPlaneClient 替换为 repositories.InitPlane(&cfg.PlaneWebApi)。
  2. services/feedbackServices.go:GetFeedbackHistory / SubmitFeedback 增加 repositories.Plane == nil 防御。
  3. cmd/mcp/tools/data/plane_v2_api_handlers.go:新增 requireSlugProject 辅助函数,修复 15 个 handler 忽略 workspace_slug/project_id 必填校验的问题。

验证要点:

  • FAIL-1 已消除:service_feedback_history → PASS(返回 506B 合法结果)。
  • B-1 已修复:data_fwh_query_site_list、data_fwh_query_tou_template_list、service_tou_template_list 等 FWH 远程查询 → PASS。
  • D 已修复:plane_list_labels/states/projects 缺参时返回清晰的"参数 workspace_slug 必填",不再是 404。
  • L4 脱敏仍 4/4 PASS,修复未影响脱敏。
  • 剩余 SKIP:61 项需真实样本参数(非缺陷)、2 项统计集合空数据(非缺陷)、1 项 service_background_task_list(B-2,inspector 未初始化,属 P2 待定项,本次未改)。

七、第二轮:真实样本补测 + MinIO/文件写读闭环

使用用户提供的样本(SN 10130095A00X25280030、GICCID 8931084524121848556、 Plane workspace fwh / project 55efdd5f-...)补测原 SKIP-A 用例,并做 MinIO 写 + 文件读闭环。

结果:149 用例 PASS 82 / FAIL 0 / SKIP 67(较上一轮 +25 用例、+25 PASS)。

  • L2S 真实样本(21/21 PASS):站点详情/功率流/模式/告警、SIM 详情/月用量、 Plane 项目/工作项/标签/状态/周期/模块/收件箱/成员 全部真实调用成功。
  • L3RW 写读闭环(4/4 PASS):storage_file_write(写入 MinIO)→ storage_file_list → storage_file_read(读回内容一致)→ storage_minio_list_objects(确认对象落盘)。
  • PII 端到端(7/7 PASS):新增 3 个真实 SN/GICCID 详情(site_query_info / sim_query / smarthub_card_info)脱敏检测,均无明文泄露。

本轮新发现缺陷(已修复)

BUG-2 data_fwh_query_system_update_log 反序列化失败

  • 现象:json: cannot unmarshal number into Go struct field SystemUpdateRecordInfo.result.upgradeType of type string
  • 根因:domain/fwhProDomain.go 中 SystemUpdateRecordInfo.UpgradeType 声明为 string, 但 FWH 上游 API 对该字段实际返回 number,导致整个响应反序列化失败、工具不可用。
  • 修复:将 UpgradeType string 改为 UpgradeType int(上游该字段返回数值,用数值类型更明确、类型安全;该字段仅透传输出、无消费方,改动安全)。
  • 验证:修复后 data_fwh_query_system_update_log → PASS。

说明:该缺陷仅在特定 SN(upgradeType 为数字)时触发,第一轮因无样本被 SKIP 掩盖, 说明真实样本补测对发现"数据形态相关"的反序列化 bug 很有价值。建议排查同族结构 (如 AHubUpdateRecordInfo、SystemUpdateDetailInfo 等)是否也存在 number/string 混用字段。


八、第三轮:级联补测(L2C + L2X),覆盖率最大化

在样本轮基础上新增两阶段自动补测,把"有必填参数但可从已 PASS 接口派生"的工具全部覆盖,无需额外外部样本。

  • L2C 级联补测(12 PASS / 5 SKIP):先调列表接口,用 jsonExtract(支持 data.list[0].iccid 路径)提取第一个 id/key/name,再喂给详情接口。覆盖: auto_update get_package/get_config、tou detail(service + data_fwh)、site_analysis get_config/data、 batch_download path、site_monitor config_get、acceleronix card_info/data_pool_sim_info、 minio read_object(正确的大小校验判 PASS)、plane get_intake_issue。
  • L2X 额外补测(7 PASS):递归级联 / 自造输入。覆盖: site_power_flow(样本 SN+date)、plane get_workspace/list_pages/list_stickies、 batch_download file_tree(递归取 path 树第一个文件叶子)、 storage_file_read_to_local(写后读到本地)、storage_file_upload(自造本地临时文件上传)。

累计结果:173 用例 PASS 101 / FAIL 0 / SKIP 72(可补测工具已全部覆盖)。

剩余 SKIP 的性质(已无"可补测"项)

排除各阶段已 PASS 后,剩 23 个工具,全部无法靠测试手段自行解决:

  • B 组(13)需"确有数据"的样本:快照 file_name、有快照的 sn+date、真实 alarm_code、 有效 cmd_type、有故障录波/EMS/告警的 SN、有升级详情的 order、有用量的 month。
  • C 组(1)需代码改动:service_background_task_list 需在 MCP main.go 初始化 asynq inspector。
  • D 组(9)环境数据为空:告警统计集合当日无文档;Plane 该 project 无 issue/cycle/module/page。 工具逻辑正常,需先有业务数据。

每个剩余 SKIP 的"缺什么/如何补"已在 report.md 的「SKIP 逐条分析」章节动态列出。


九、已确认无问题的项(供对照)

  • 协议与 119 个工具注册、schema、命名规范:全部正常。
  • 参数校验(L1 14 项):全部正确拦截。
  • PII 脱敏(L4):生产库真实数据经 service_site_search/service_sim_search/data_mongo_find 返回均正确脱敏(account/手机/地址/经纬度/IMEI/IMSI/deviceIp 均 ******),保留字段(sn/giccid/_id)正常;helper 单测 21 项全 PASS。脱敏是本次测试重点,结论为安全达标。