将 test/pre-mcp-discovery 分支上多位同学的能力改动整体合入 main, 服务发现端点保持生产 https://mcp.dingtalk.com,未携带分支上的预发改动。 主要内容: - 修雨域(chat / report / todo / contact / mail)对齐 wukong 基线 (#355) - DWS 内容生产能力增强 (#387)、文档导出与 OA 对齐 (#362 #388 #390) - envelope 注册 cli.Aliases 为 cobra aliases、命令结构与 JSON 解析优化 - skills 多包同步 wukong 对齐内容(attendance/calendar/minutes/oa/sheet 等) - product/oa 进一步对齐 wukong 排除项:未携带 566b4e1(test: point service discovery to pre-mcp.dingtalk.com); 以下 4 个常量保持生产:skill_command.go / auth/endpoints.go / cli/loader.go / market/registry.go。 Co-authored-by: john <32427341+wqyenjoy@users.noreply.github.com> Co-authored-by: wxianfeng <wang.fl1429@gmail.com>
281 lines
10 KiB
Go
281 lines
10 KiB
Go
// Copyright 2026 Alibaba Group
|
||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||
// you may not use this file except in compliance with the License.
|
||
// You may obtain a copy of the License at
|
||
//
|
||
// http://www.apache.org/licenses/LICENSE-2.0
|
||
//
|
||
// Unless required by applicable law or agreed to in writing, software
|
||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
// See the License for the specific language governing permissions and
|
||
// limitations under the License.
|
||
|
||
package helpers
|
||
|
||
import (
|
||
"encoding/json"
|
||
"reflect"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// TestEnrichReportListContent_InboxStripsContent — inbox callers must never
|
||
// surface 日志内容 either in agentDisplayColumns or per-row maps; the privacy
|
||
// contract is enforced by the column set and the row builder simultaneously.
|
||
func TestEnrichReportListContent_InboxStripsContent(t *testing.T) {
|
||
content := map[string]any{
|
||
"result": map[string]any{
|
||
"list": []any{
|
||
map[string]any{
|
||
"reportId": "R-1",
|
||
"reportName": "周报",
|
||
"creatorName": "张三",
|
||
"createTime": float64(1747900800000), // 2025-05-22T16:00:00 UTC; locale-dependent display
|
||
"readStatus": false,
|
||
"content": "本周完成 enrichment helpers",
|
||
"dingtalkOpenUrl": "https://example.dingtalk.com/r/1",
|
||
},
|
||
},
|
||
"hasMore": false,
|
||
"nextCursor": float64(0),
|
||
},
|
||
}
|
||
got := EnrichReportListContent(content, false)
|
||
if got["agentDisplayContentIncluded"] != false {
|
||
t.Fatalf("inbox includeContent should be false, got %v", got["agentDisplayContentIncluded"])
|
||
}
|
||
cols, _ := got["agentDisplayColumns"].([]string)
|
||
want := []string{"日期", "标题", "发送人", "状态", "钉钉链接"}
|
||
if !reflect.DeepEqual(cols, want) {
|
||
t.Fatalf("inbox columns mismatch:\n got=%v\nwant=%v", cols, want)
|
||
}
|
||
md, _ := got["agentDisplayMarkdown"].(string)
|
||
if !strings.HasPrefix(md, "| 日期 | 标题 | 发送人 | 状态 | 钉钉链接 |") {
|
||
t.Fatalf("inbox markdown header missing, got: %q", md)
|
||
}
|
||
if strings.Contains(md, "日志内容") {
|
||
t.Fatalf("inbox markdown must NOT contain 日志内容 column, got: %q", md)
|
||
}
|
||
// result list rows must not carry 日志内容 either — the test suite has
|
||
// `assert all("日志内容" not in item for item in data["result"])`.
|
||
wrapper, _ := got["result"].(map[string]any)
|
||
if wrapper == nil {
|
||
t.Fatalf("inbox result should be wrapper map preserving hasMore, got: %T", got["result"])
|
||
}
|
||
list, _ := wrapper["list"].([]any)
|
||
if len(list) != 1 {
|
||
t.Fatalf("expected 1 row in result.list, got %d", len(list))
|
||
}
|
||
row, _ := list[0].(map[string]any)
|
||
if _, present := row["日志内容"]; present {
|
||
t.Fatalf("inbox row must not contain 日志内容 key, got: %v", row)
|
||
}
|
||
if got["count"] != 1 {
|
||
t.Fatalf("count should be 1, got %v", got["count"])
|
||
}
|
||
// hasMore / nextCursor preserved from the original wrapper.
|
||
if wrapper["hasMore"] != false {
|
||
t.Fatalf("hasMore not preserved: %v", wrapper["hasMore"])
|
||
}
|
||
}
|
||
|
||
// TestEnrichReportListContent_OutboxKeepsContent — outbox callers (author ==
|
||
// self) get the 日志内容 column and per-row body.
|
||
func TestEnrichReportListContent_OutboxKeepsContent(t *testing.T) {
|
||
content := map[string]any{
|
||
"result": map[string]any{
|
||
"list": []any{
|
||
map[string]any{
|
||
"reportId": "R-9",
|
||
"reportName": "日报",
|
||
"creatorName": "李四",
|
||
"createTime": float64(1747900800000),
|
||
"readStatus": true,
|
||
"content": "完成 outbox enrichment 测试",
|
||
"dingtalkOpenUrl": "https://example.dingtalk.com/r/9",
|
||
},
|
||
},
|
||
},
|
||
}
|
||
got := EnrichReportListContent(content, true)
|
||
if got["agentDisplayContentIncluded"] != true {
|
||
t.Fatalf("outbox includeContent should be true")
|
||
}
|
||
cols, _ := got["agentDisplayColumns"].([]string)
|
||
want := []string{"日期", "标题", "发送人", "状态", "日志内容", "钉钉链接"}
|
||
if !reflect.DeepEqual(cols, want) {
|
||
t.Fatalf("outbox columns mismatch:\n got=%v\nwant=%v", cols, want)
|
||
}
|
||
md, _ := got["agentDisplayMarkdown"].(string)
|
||
if !strings.HasPrefix(md, "| 日期 | 标题 | 发送人 | 状态 | 日志内容 | 钉钉链接 |") {
|
||
t.Fatalf("outbox markdown header wrong, got: %q", md)
|
||
}
|
||
if !strings.Contains(md, "[在钉钉中查看日志](") {
|
||
t.Fatalf("outbox markdown must contain REPORT_LINK_MARKER prefix when row has a URL, got: %q", md)
|
||
}
|
||
if !strings.Contains(md, "完成 outbox enrichment 测试") {
|
||
t.Fatalf("outbox markdown must contain the 日志内容 cell text, got: %q", md)
|
||
}
|
||
}
|
||
|
||
// TestEnrichReportListContent_EmptyListStillEmitsSchema — even on a zero-row
|
||
// response the agent-display fields must be present so downstream tooling
|
||
// has a stable schema. count=0 / markdown is header-only.
|
||
func TestEnrichReportListContent_EmptyListStillEmitsSchema(t *testing.T) {
|
||
content := map[string]any{
|
||
"result": map[string]any{
|
||
"list": []any{},
|
||
},
|
||
}
|
||
got := EnrichReportListContent(content, false)
|
||
if got["count"] != 0 {
|
||
t.Fatalf("count should be 0, got %v", got["count"])
|
||
}
|
||
md, _ := got["agentDisplayMarkdown"].(string)
|
||
if !strings.HasPrefix(md, "| 日期 | 标题 | 发送人 | 状态 | 钉钉链接 |") {
|
||
t.Fatalf("empty inbox should still emit header row, got: %q", md)
|
||
}
|
||
// Should not panic / lose schema fields.
|
||
for _, k := range []string{"agentDisplayContentIncluded", "agentDisplayColumns", "agentDisplayMarkdown", "success"} {
|
||
if _, ok := got[k]; !ok {
|
||
t.Fatalf("missing key %q on empty list response", k)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestEnrichReportListContent_NilOrUnknownPassthrough — nil / non-list maps
|
||
// must not be mutated; the agent layer treats absence of agentDisplay* as
|
||
// "no enrichment available", which is the safe fallback.
|
||
func TestEnrichReportListContent_NilOrUnknownPassthrough(t *testing.T) {
|
||
if got := EnrichReportListContent(nil, true); got != nil {
|
||
t.Fatalf("nil input should return nil")
|
||
}
|
||
// No list-shaped key — we still attach the agent display schema so
|
||
// downstream callers always see a uniform shape, but the rows are empty.
|
||
src := map[string]any{"unrelated": "value"}
|
||
got := EnrichReportListContent(src, false)
|
||
if got["unrelated"] != "value" {
|
||
t.Fatalf("unrelated keys must be preserved")
|
||
}
|
||
if got["count"] != 0 {
|
||
t.Fatalf("missing list -> count should be 0")
|
||
}
|
||
// The input map must not be mutated in place.
|
||
if _, present := src["agentDisplayColumns"]; present {
|
||
t.Fatalf("EnrichReportListContent mutated the input map")
|
||
}
|
||
}
|
||
|
||
// TestEnrichCapturedReportListJSON_PassthroughForDryRun — dry-run output
|
||
// (the {invocation, response} envelope) must round-trip verbatim through
|
||
// the wrapper; enrichment only applies to unwrapped MCP content responses.
|
||
func TestEnrichCapturedReportListJSON_PassthroughForDryRun(t *testing.T) {
|
||
raw := []byte(`{
|
||
"invocation": {"kind":"compat_invocation"},
|
||
"response": {"dry_run": true}
|
||
}`)
|
||
if _, ok := enrichCapturedReportListJSON(raw, false); ok {
|
||
t.Fatalf("dry-run envelope must fall through to passthrough")
|
||
}
|
||
}
|
||
|
||
// TestEnrichCapturedReportListJSON_HandlesContentShape — when output.
|
||
// WriteCommandPayload unwraps the Result and emits the raw `content` map,
|
||
// the wrapper must detect that shape and enrich it.
|
||
func TestEnrichCapturedReportListJSON_HandlesContentShape(t *testing.T) {
|
||
raw := []byte(`{
|
||
"success": true,
|
||
"result": {
|
||
"list": [
|
||
{
|
||
"reportId": "X-1",
|
||
"reportName": "周报",
|
||
"creatorName": "王五",
|
||
"createTime": 1747900800000,
|
||
"readStatus": true,
|
||
"dingtalkOpenUrl": "https://example.dingtalk.com/r/x1"
|
||
}
|
||
]
|
||
}
|
||
}`)
|
||
got, ok := enrichCapturedReportListJSON(raw, true)
|
||
if !ok {
|
||
t.Fatalf("content shape should be recognised")
|
||
}
|
||
root, _ := got.(map[string]any)
|
||
if root["agentDisplayContentIncluded"] != true {
|
||
t.Fatalf("outbox enrichment should mark contentIncluded=true")
|
||
}
|
||
cols, _ := root["agentDisplayColumns"].([]string)
|
||
if len(cols) != 6 {
|
||
t.Fatalf("outbox columns should have 6 entries, got %d (%v)", len(cols), cols)
|
||
}
|
||
}
|
||
|
||
// TestEnrichCapturedReportListJSON_MalformedJSON — invalid JSON must not
|
||
// panic and must fall through so the caller writes the raw bytes back.
|
||
func TestEnrichCapturedReportListJSON_MalformedJSON(t *testing.T) {
|
||
if _, ok := enrichCapturedReportListJSON([]byte(`not json`), false); ok {
|
||
t.Fatalf("malformed JSON must fall through to passthrough")
|
||
}
|
||
if _, ok := enrichCapturedReportListJSON([]byte(``), false); ok {
|
||
t.Fatalf("empty buffer must fall through to passthrough")
|
||
}
|
||
}
|
||
|
||
// TestEnrichReportListContent_MarkdownEscapesPipes — embedded pipes in user
|
||
// content must be replaced with full-width "|" so the markdown table is
|
||
// not broken at the renderer.
|
||
func TestEnrichReportListContent_MarkdownEscapesPipes(t *testing.T) {
|
||
content := map[string]any{
|
||
"result": map[string]any{
|
||
"list": []any{
|
||
map[string]any{
|
||
"reportId": "R-pipe",
|
||
"reportName": "标题|含管道",
|
||
"creatorName": "张|三",
|
||
"createTime": float64(1747900800000),
|
||
"readStatus": false,
|
||
},
|
||
},
|
||
},
|
||
}
|
||
got := EnrichReportListContent(content, false)
|
||
md, _ := got["agentDisplayMarkdown"].(string)
|
||
if strings.Contains(strings.Split(md, "\n")[2], "|含管道") {
|
||
// The first data row is line index 2 (header, divider, data).
|
||
t.Fatalf("raw '|' must be replaced with full-width '|' in cell, got: %q", md)
|
||
}
|
||
if !strings.Contains(md, "标题|含管道") {
|
||
t.Fatalf("expected escaped full-width pipe in title cell, got: %q", md)
|
||
}
|
||
}
|
||
|
||
// TestEnrichReportListContent_JSONNumberTimestamps — json.Number variants of
|
||
// the timestamp keys must still produce a formatted 日期 cell.
|
||
func TestEnrichReportListContent_JSONNumberTimestamps(t *testing.T) {
|
||
decoded := map[string]any{}
|
||
dec := json.NewDecoder(strings.NewReader(`{
|
||
"result": {
|
||
"list": [{
|
||
"reportId": "N-1",
|
||
"reportName": "N报",
|
||
"createTime": 1747900800000
|
||
}]
|
||
}
|
||
}`))
|
||
dec.UseNumber()
|
||
if err := dec.Decode(&decoded); err != nil {
|
||
t.Fatalf("decode: %v", err)
|
||
}
|
||
got := EnrichReportListContent(decoded, false)
|
||
wrapper, _ := got["result"].(map[string]any)
|
||
list, _ := wrapper["list"].([]any)
|
||
row, _ := list[0].(map[string]any)
|
||
date, _ := row["日期"].(string)
|
||
if date == "" {
|
||
t.Fatalf("json.Number timestamp should still yield a formatted 日期, got empty")
|
||
}
|
||
}
|