Files
dingtalk-workspace-cli/internal/helpers/report_test.go
玉澜 0fbdfe0130 test(seam): make testseam the only seam-swap form and legislate it
- Add testseam.Protect for save-and-restore seams with no up-front stub
  value (e.g. os.Args mutated by the code under test).
- Migrate all 35 remaining manual prev/assign/t.Cleanup-restore trios to
  testseam.Swap/Protect across app, auth, cli, event, helpers, output,
  pipeline, and shortcut tests; restores can no longer be forgotten.
- check-schema-catalog.sh: fail closed when a manual seam restore
  reappears in any *_test.go (internal/testseam exempt).

Seam injection is now a mechanism, not a convention.
2026-08-03 20:00:47 +08:00

458 lines
19 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package helpers
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
"github.com/spf13/cobra"
)
type reportTestCaller struct {
dry bool
format string
response string
err error
calls []string
}
func (c *reportTestCaller) CallTool(_ context.Context, _, tool string, _ map[string]any) (*edition.ToolResult, error) {
c.calls = append(c.calls, tool)
if c.err != nil {
return nil, c.err
}
return &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: c.response}}}, nil
}
func (c *reportTestCaller) Format() string { return c.format }
func (c *reportTestCaller) DryRun() bool { return c.dry }
func (*reportTestCaller) Fields() string { return "" }
func (*reportTestCaller) JQ() string { return "" }
func installReportTestDeps(t *testing.T, caller *reportTestCaller) (*bytes.Buffer, *bytes.Buffer) {
t.Helper()
testseam.Protect(t, &deps)
InitDeps(caller)
out := &bytes.Buffer{}
errOut := &bytes.Buffer{}
deps.Out.w = out
deps.Out.errW = errOut
return out, errOut
}
func TestCrossPlatformCoverageReportReadableListEnrichment(t *testing.T) {
caller := &reportTestCaller{format: "json", response: `{
"result":{"reportName":"Detail","creatorName":"Alice","createTime":1700000000000,"readStatus":true,"url":"dingtalk://detail","contents":[{"key":"Done","content":"A|B"}]}}
`}
installReportTestDeps(t, caller)
body := map[string]any{
"success": true,
"hasMore": false,
"nextCursor": "next",
"result": []any{
map[string]any{"reportId": "older", "createTime": float64(1600000000000), "senderName": "Old"},
map[string]any{"report_id": "newer", "createTime": "2026-01-02T03:04:05Z"},
map[string]any{"title": "no id"},
},
}
got := enrichReportListReadable(context.Background(), "sent", body).(map[string]any)
rows := got["result"].([]map[string]string)
if len(rows) != 2 || got["count"] != 2 || !got["agentDisplayContentIncluded"].(bool) {
t.Fatalf("enriched sent list = %#v", got)
}
if !strings.Contains(got["agentDisplayMarkdown"].(string), "A|B") || len(caller.calls) != 2 {
t.Fatalf("markdown/calls = %q / %#v", got["agentDisplayMarkdown"], caller.calls)
}
if got["nextCursor"] != "next" || got["hasMore"] != false {
t.Fatalf("pagination = %#v", got)
}
caller.err = errors.New("detail unavailable")
inbox := enrichReportListReadable(context.Background(), "list", map[string]any{
"result": map[string]any{"items": []any{map[string]any{"reportId": "id"}}, "cursor": "nested"},
}).(map[string]any)
if inbox["agentDisplayContentIncluded"].(bool) || inbox["cursor"] != "nested" {
t.Fatalf("enriched inbox = %#v", inbox)
}
if enrichReportListReadable(context.Background(), "list", []any{"not-map"}) == nil {
t.Fatal("non-map list enrichment changed value")
}
plain := map[string]any{"success": "ok"}
if got := enrichReportListReadable(context.Background(), "list", plain).(map[string]any); got["success"] != "ok" || len(got) != 1 {
t.Fatalf("map without list was changed: %#v", got)
}
}
func TestCrossPlatformCoverageReportReadableValueFormatting(t *testing.T) {
columns := reportListDisplayColumns("sent")
if len(columns) != 6 || len(reportListDisplayColumns("list")) != 5 {
t.Fatal("display columns changed")
}
if !strings.Contains(reportListAgentDisplayInstruction(true, "header"), "日志内容") ||
!strings.Contains(reportListAgentDisplayInstruction(false, "header"), "禁止返回日志正文") {
t.Fatal("display instructions changed")
}
if reportReadableListTitle("sent") == reportReadableListTitle("list") {
t.Fatal("sent and inbox titles should differ")
}
if reportListSuccess(map[string]any{}) != true || reportListSuccess(map[string]any{"success": false}) != false {
t.Fatal("reportListSuccess changed")
}
values := []any{
" text ", float64(1.25), true, false,
[]any{"a", float64(2), nil}, map[string]any{"text": "nested"}, nil,
}
for _, value := range values {
_ = reportDisplayStringFromValue(value)
_ = reportReadableContentFromValue(value)
}
content := reportReadableContentFromValue([]any{
map[string]any{"key": "Done", "content": "work"},
map[string]any{"value": "bare"},
map[string]any{"key": "empty"},
})
if content != "Done:work;bare" {
t.Fatalf("readable content = %q", content)
}
if reportFirstDisplayValue(map[string]any{"first": nil, "second": "ok"}, "first", "second") != "ok" {
t.Fatal("reportFirstDisplayValue did not fall through")
}
if reportCompactDisplayText(" a | b \n c ", 4) != "a | ..." || reportCompactDisplayText("short", 0) != "short" {
t.Fatal("compact display formatting changed")
}
row := reportReadableRow(map[string]any{"creatorName": "Alice", "read": false})
if row["标题"] != "Alice的日志" || row["状态"] != "未读" {
t.Fatalf("readable row = %#v", row)
}
if reportReadableRow(map[string]any{})["标题"] != "日志" {
t.Fatal("empty row title fallback changed")
}
for _, status := range []any{true, false, "read", "unread", "custom", float64(1), float64(0), float64(2), nil} {
_ = reportStatusValueToString(status)
}
timestamps := []any{float64(1700000000), int64(1700000000000), int(1700000000), json.Number("1700000000"), json.Number("bad"), "1700000000", "2026-01-02T03:04:05Z", "2026-01-02 03:04", "bad", "", nil}
for _, timestamp := range timestamps {
_ = reportTimeValueToMillis(timestamp)
_ = reportTimeValueToString(timestamp)
}
if normalizeReportTimestamp(-1) != 0 || normalizeReportTimestamp(1700000000) != 1700000000000 || reportMillisToLocalString(0) != "" {
t.Fatal("timestamp normalization changed")
}
entries := makeReportReadableEntries([]map[string]any{
{"reportId": "zero", "title": "Z"},
{"reportId": "old", "createTime": float64(1600000000000)},
{"reportId": "new", "createTime": float64(1700000000000)},
{"reportId": "same-a", "createTime": "date-b"},
{"reportId": "same-b", "createTime": "date-a"},
})
sortReportReadableEntries(entries)
if entries[0].reportID != "new" || entries[len(entries)-1].reportID != "zero" {
t.Fatalf("sorted entries = %#v", entries)
}
table := reportListMarkdownTable([]string{"A", "B"}, []map[string]string{{"A": "x|y\nnext", "B": "z"}})
if !strings.Contains(table, "x|y next") || reportListMarkdownHeader([]string{"A"}) != "| A |" {
t.Fatalf("markdown table = %q", table)
}
}
func TestCrossPlatformCoverageReportResponseTraversalAndLinks(t *testing.T) {
detail := map[string]any{"result": map[string]any{
"report_name": "Title", "creatorName": "Alice", "createTime": "2026-01-02T03:04:05Z",
"readStatus": "read", "contents": []any{map[string]any{"key": "Done", "content": "work"}},
"url": "dingtalk://report",
}}
if reportReadableDetailTitle(detail) != "Title" || reportReadableDetailSender(detail) != "Alice" ||
reportReadableDetailDate(detail) == "" || reportReadableDetailStatus(detail) != "已读" ||
reportReadableDetailContent(detail) != "Done:work" || !strings.Contains(reportMarkdownLinkFromResponse(detail), "dingtalk://report") {
t.Fatalf("detail traversal failed: %#v", detail)
}
if reportStringFromResponse(map[string]any{"dingtalkOpenMarkdownLink": " [link](url) "}, "dingtalkOpenMarkdownLink") != "[link](url)" {
t.Fatal("reportStringFromResponse trimming changed")
}
for _, value := range []any{
map[string]any{"result": map[string]any{"dingtalkOpenUrl": " nested "}},
map[string]any{"result": map[string]any{"dingtalkOpenLink": map[string]any{"url": " nested-link "}}},
map[string]any{"url": " top "},
map[string]any{"dingtalkOpenLink": map[string]any{"url": " top-link "}},
[]any{},
} {
_ = reportURLFromResponse(value)
}
body := map[string]any{"result": map[string]any{}}
attachReportURL(body, "dingtalk://url")
if body["url"] != "dingtalk://url" || body["result"].(map[string]any)["url"] != "dingtalk://url" {
t.Fatalf("attachReportURL = %#v", body)
}
if reportDingtalkOpenLink("url")["url"] != "url" || !strings.Contains(reportDingtalkMarkdownLink("url"), "url") {
t.Fatal("report link helpers changed")
}
ids := []map[string]any{{"reportId": " id "}, {"reportID": "id"}, {"report_id": "id"}, {"report_Id": "id"}, {"report-id": "id"}, {"reportId": 1}}
for index, item := range ids {
got := reportIDFromMap(item)
if index < 5 && got != "id" {
t.Errorf("reportIDFromMap(%#v) = %q", item, got)
}
}
if reportIDFromCreateResponse(map[string]any{"result": " id "}) != "id" ||
reportIDFromCreateResponse(map[string]any{"result": map[string]any{"report_id": "nested"}}) != "nested" ||
reportIDFromCreateResponse(map[string]any{"reportId": "top"}) != "top" ||
reportIDFromCreateResponse(map[string]any{}) != "" {
t.Fatal("create response ID extraction changed")
}
}
func TestCrossPlatformCoverageReportCreateEnrichmentBranches(t *testing.T) {
caller := &reportTestCaller{format: "json", response: `{"result":{"url":"dingtalk://detail"}}`}
out, _ := installReportTestDeps(t, caller)
if enrichReportCreateWithDetailURL(context.Background(), []any{"unchanged"}) == nil {
t.Fatal("non-map create response changed")
}
direct := enrichReportCreateWithDetailURL(context.Background(), map[string]any{"url": "dingtalk://direct"}).(map[string]any)
if direct["dingtalkOpenUrl"] != "dingtalk://direct" {
t.Fatalf("direct URL enrichment = %#v", direct)
}
missing := enrichReportCreateWithDetailURL(context.Background(), map[string]any{}).(map[string]any)
if missing["urlLookupError"] == nil {
t.Fatal("missing report ID had no lookup error")
}
resolved := enrichReportCreateWithDetailURL(context.Background(), map[string]any{"reportId": "id"}).(map[string]any)
if resolved["url"] != "dingtalk://detail" {
t.Fatalf("detail URL enrichment = %#v", resolved)
}
caller.response = "not-json"
invalid := enrichReportCreateWithDetailURL(context.Background(), map[string]any{"reportId": "id"}).(map[string]any)
if !strings.Contains(invalid["urlLookupError"].(string), "parse") {
t.Fatalf("invalid detail response = %#v", invalid)
}
caller.response = `{}`
noURL := enrichReportCreateWithDetailURL(context.Background(), map[string]any{"reportId": "id"}).(map[string]any)
if noURL["urlLookupError"] == nil {
t.Fatalf("detail without URL = %#v", noURL)
}
caller.err = errors.New("lookup failed")
failed := enrichReportCreateWithDetailURL(context.Background(), map[string]any{"reportId": "id"}).(map[string]any)
if !strings.Contains(failed["urlLookupError"].(string), "lookup failed") {
t.Fatalf("detail lookup error = %#v", failed)
}
caller.err = nil
caller.response = `{}`
out.Reset()
if err := printReportCreateWithDetailURL(context.Background(), "not-json"); err != nil || out.String() != "not-json\n" {
t.Fatalf("raw create output = %q, %v", out.String(), err)
}
out.Reset()
if err := printReportCreateWithDetailURL(context.Background(), `{}`); err != nil || !strings.Contains(out.String(), "urlLookupError") {
t.Fatalf("JSON create output = %q, %v", out.String(), err)
}
}
func TestCrossPlatformCoverageCallReportCreateWithDetailURLCoverage(t *testing.T) {
caller := &reportTestCaller{dry: true, format: "json", response: `{"ok":true}`}
installReportTestDeps(t, caller)
_ = callReportCreateWithDetailURL(map[string]any{"template_id": "template"})
caller.dry = false
caller.response = `{"reportId":"id","url":"dingtalk://direct"}`
if err := callReportCreateWithDetailURL(map[string]any{"template_id": "template"}); err != nil {
t.Fatal(err)
}
caller.err = errors.New("create failed")
if err := callReportCreateWithDetailURL(map[string]any{}); err == nil {
t.Fatal("report create error was ignored")
}
}
func TestCrossPlatformCoverageReportContentsValidationMatrix(t *testing.T) {
valid := map[string]any{"key": " Done ", "sort": 1, "content": "", "type": "text", "contentType": "text"}
if err := validateAndNormalizeReportContents([]map[string]any{valid}); err != nil {
t.Fatalf("valid contents error = %v", err)
}
if valid["key"] != "Done" || valid["sort"] != "1" || valid["type"] != "1" || valid["contentType"] != "markdown" {
t.Fatalf("normalized contents = %#v", valid)
}
validNonText := map[string]any{"key": "Number", "sort": json.Number("2"), "content": "3", "type": "number", "contentType": "raw"}
if err := validateAndNormalizeReportContents([]map[string]any{validNonText}); err != nil || validNonText["contentType"] != "origin" {
t.Fatalf("valid non-text = %#v, %v", validNonText, err)
}
invalid := [][]map[string]any{
nil, {nil},
{{"sort": 1, "content": "x", "type": "1", "contentType": "markdown"}},
{{"key": 1, "sort": 1, "content": "x", "type": "1", "contentType": "markdown"}},
{{"key": " ", "sort": 1, "content": "x", "type": "1", "contentType": "markdown"}},
{{"key": "k", "content": "x", "type": "1", "contentType": "markdown"}},
{{"key": "k", "sort": map[string]any{}, "content": "x", "type": "1", "contentType": "markdown"}},
{{"key": "k", "sort": " ", "content": "x", "type": "1", "contentType": "markdown"}},
{{"key": "k", "sort": "bad", "content": "x", "type": "1", "contentType": "markdown"}},
{{"key": "k", "sort": 1, "type": "1", "contentType": "markdown"}},
{{"key": "k", "sort": 1, "content": 1, "type": "1", "contentType": "markdown"}},
{{"key": "k", "sort": 1, "content": "x", "contentType": "markdown"}},
{{"key": "k", "sort": 1, "content": "x", "type": map[string]any{}, "contentType": "markdown"}},
{{"key": "k", "sort": 1, "content": "x", "type": "bad", "contentType": "markdown"}},
{{"key": "k", "sort": 1, "content": "x", "type": "1"}},
{{"key": "k", "sort": 1, "content": "x", "type": "1", "contentType": 1}},
{{"key": "k", "sort": 1, "content": "x", "type": "1", "contentType": "origin"}},
{{"key": "k", "sort": 1, "content": "x", "type": "2", "contentType": "markdown"}},
}
for index, contents := range invalid {
if err := validateAndNormalizeReportContents(contents); err == nil {
t.Errorf("invalid contents case %d returned nil", index)
}
}
for _, value := range []any{"1", float64(1), float64(1.5), float32(1), float32(1.5), int(1), int64(1), int32(1), json.Number("1"), json.Number("1.5"), true} {
_, _ = reportScalarToString(value)
}
for _, value := range []any{"1", "text", "number", "single", "date", "multi", "image", "attachment", "bad", true} {
_, _ = normalizeReportFieldType(value, 0)
}
}
type failingReader struct{}
func (failingReader) Read([]byte) (int, error) { return 0, errors.New("read failed") }
func TestCrossPlatformCoverageReportContentsSourcesAndSafePaths(t *testing.T) {
dir := t.TempDir()
previousWD, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
if err := os.Chdir(dir); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = os.Chdir(previousWD) })
if err := os.WriteFile("report.json", []byte(`[{}]`), 0o600); err != nil {
t.Fatal(err)
}
safe, err := resolveSafeReportContentsFilePath("report.json")
if err != nil || !filepath.IsAbs(safe) {
t.Fatalf("safe path = %q, %v", safe, err)
}
if got, err := readReportContentsFile(safe); err != nil || got != `[{}]` {
t.Fatalf("read report = %q, %v", got, err)
}
for _, path := range []string{".", "-", "../outside.json", "missing.json"} {
if _, err := resolveSafeReportContentsFilePath(path); err == nil {
t.Errorf("unsafe path %q returned nil error", path)
}
}
if _, err := resolveSafeReportContentsFilePath(dir); err == nil {
t.Fatal("directory path returned nil error")
}
if _, err := readReportContentsFile("missing.json"); err == nil {
t.Fatal("missing report file returned nil error")
}
if !pathEscapesUpward(filepath.Clean("..")) || !pathEscapesUpward(filepath.Clean("../x")) || pathEscapesUpward("inside") {
t.Fatal("pathEscapesUpward changed")
}
if !pathWithinRoot(dir, filepath.Join(dir, "file")) || pathWithinRoot(dir, filepath.Join(filepath.Dir(dir), "outside")) {
t.Fatal("pathWithinRoot changed")
}
outside := filepath.Join(t.TempDir(), "outside.json")
if err := os.WriteFile(outside, []byte(`[]`), 0o600); err != nil {
t.Fatal(err)
}
if err := os.Symlink(outside, "link.json"); err == nil {
if _, err := resolveSafeReportContentsFilePath("link.json"); err == nil {
t.Fatal("symlink escaping cwd returned nil error")
}
}
if got, err := readReportContentsStdin(strings.NewReader("stdin")); err != nil || got != "stdin" {
t.Fatalf("stdin contents = %q, %v", got, err)
}
if _, err := readReportContentsLimited(failingReader{}, "source"); err == nil {
t.Fatal("reader error was ignored")
}
if _, err := readReportContentsLimited(io.LimitReader(strings.NewReader(strings.Repeat("x", reportContentsMaxBytes+1)), int64(reportContentsMaxBytes+1)), "large"); err == nil {
t.Fatal("oversized contents returned nil error")
}
if cli, ok := reportContentsTooLargeError("large").(*CLIError); !ok || cli.Code != CodeInputTooLarge {
t.Fatal("oversized error classification changed")
}
cmd := &cobra.Command{Use: "test"}
addReportCreateFlags(cmd)
cmd.SetIn(strings.NewReader("from stdin"))
if err := cmd.Flags().Set("contents", "-"); err != nil {
t.Fatal(err)
}
if got, err := resolveReportContentsFromFlags(cmd); err != nil || got != "from stdin" {
t.Fatalf("stdin flag contents = %q, %v", got, err)
}
_ = cmd.Flags().Set("contents", "literal")
if got, err := resolveReportContentsFromFlags(cmd); err != nil || got != "literal" {
t.Fatalf("literal contents = %q, %v", got, err)
}
_ = cmd.Flags().Set("contents-file", "report.json")
if got, err := resolveReportContentsFromFlags(cmd); err != nil || got != `[{}]` {
t.Fatalf("file contents = %q, %v", got, err)
}
}
func TestCrossPlatformCoverageReportDispatchHintsAndDeprecation(t *testing.T) {
caller := &reportTestCaller{format: "table"}
out, _ := installReportTestDeps(t, caller)
for _, operation := range []string{"template-list", "template-detail", "detail", "stats", "list", "sent", "create", "unknown"} {
_ = reportDispatchHint(operation, "")
_ = reportDispatchHint(operation, "parameter error")
}
if reportDispatchHint("create", "PARAM_ERROR") == "" || reportDispatchHint("unknown", "bad") != "" {
t.Fatal("dispatch hint routing changed")
}
if err := withReportDispatchHint("list", nil); err != nil || !strings.Contains(out.String(), "可复用") {
t.Fatalf("successful dispatch hint = %q, %v", out.String(), err)
}
caller.format = "json"
if err := withReportDispatchHint("list", nil); err != nil {
t.Fatal(err)
}
plain := errors.New("plain")
if withReportDispatchHint("list", plain) != plain {
t.Fatal("plain error was wrapped")
}
cli := &CLIError{Code: CodeMCPToolError, Message: "bad"}
if got := withReportDispatchHint("list", cli); got != cli || cli.Suggestion == "" {
t.Fatalf("CLI dispatch hint = %#v", got)
}
cli.Suggestion = "existing"
_ = withReportDispatchHint("list", cli)
if !strings.Contains(cli.Suggestion, "existing\n") {
t.Fatalf("existing suggestion = %q", cli.Suggestion)
}
cmd := &cobra.Command{Use: "old"}
errOut := &bytes.Buffer{}
cmd.SetErr(errOut)
called := false
run := withReportDeprecationWarning("old", "new", func(*cobra.Command, []string) error { called = true; return nil })
if err := run(cmd, nil); err != nil || !called || !strings.Contains(errOut.String(), "deprecated") {
t.Fatalf("deprecation wrapper = %q, %v, %v", errOut.String(), called, err)
}
if got := parseReportUserIDs(" a, ,b "); len(got) != 2 || got[0] != "a" || got[1] != "b" {
t.Fatalf("parseReportUserIDs = %#v", got)
}
_ = time.Now() // keep time-zone dependent helpers exercised in this test file.
}