405 lines
14 KiB
Go
405 lines
14 KiB
Go
package helpers
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
|
|
)
|
|
|
|
type docReadScopeCall struct {
|
|
server string
|
|
tool string
|
|
args map[string]any
|
|
}
|
|
|
|
type docReadScopeCaller struct {
|
|
calls []docReadScopeCall
|
|
text string
|
|
err error
|
|
format string
|
|
}
|
|
|
|
func (c *docReadScopeCaller) CallTool(_ context.Context, server, tool string, args map[string]any) (*edition.ToolResult, error) {
|
|
c.calls = append(c.calls, docReadScopeCall{server: server, tool: tool, args: args})
|
|
if c.err != nil {
|
|
return nil, c.err
|
|
}
|
|
return &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: c.text}}}, nil
|
|
}
|
|
|
|
func (c *docReadScopeCaller) Format() string {
|
|
if c.format == "" {
|
|
return "json"
|
|
}
|
|
return c.format
|
|
}
|
|
func (*docReadScopeCaller) DryRun() bool { return false }
|
|
func (*docReadScopeCaller) Fields() string { return "" }
|
|
func (*docReadScopeCaller) JQ() string { return "" }
|
|
|
|
func installDocReadScopeCaller(t *testing.T, caller *docReadScopeCaller) *bytes.Buffer {
|
|
t.Helper()
|
|
previousDeps := deps
|
|
previousArgs := os.Args
|
|
var output bytes.Buffer
|
|
InitDeps(caller)
|
|
deps.Out.w = &output
|
|
deps.Out.errW = &output
|
|
os.Args = []string{"dws", "doc"}
|
|
t.Cleanup(func() {
|
|
deps = previousDeps
|
|
os.Args = previousArgs
|
|
})
|
|
return &output
|
|
}
|
|
|
|
func executeDocReadScopeCommand(t *testing.T, caller *docReadScopeCaller, args ...string) error {
|
|
t.Helper()
|
|
installDocReadScopeCaller(t, caller)
|
|
root := newDocCommand()
|
|
root.SilenceErrors = true
|
|
root.SilenceUsage = true
|
|
root.SetArgs(args)
|
|
return root.Execute()
|
|
}
|
|
|
|
func TestCrossPlatformCoverageDocReadScopeFlagsAndValidation(t *testing.T) {
|
|
root := newDocCommand()
|
|
read, remaining, err := root.Find([]string{"read"})
|
|
if err != nil || len(remaining) != 0 {
|
|
t.Fatalf("find read: remaining=%v err=%v", remaining, err)
|
|
}
|
|
for _, name := range []string{"scope", "tags", "max-depth", "start-block-id", "end-block-id", "password", "version"} {
|
|
if read.Flags().Lookup(name) == nil {
|
|
t.Fatalf("doc read missing --%s", name)
|
|
}
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
args []string
|
|
want string
|
|
}{
|
|
{
|
|
name: "scope needs jsonml",
|
|
args: []string{"read", "--node", "doc-1", "--scope", "outline"},
|
|
want: "--scope/--tags requires",
|
|
},
|
|
{
|
|
name: "tags need scope",
|
|
args: []string{"read", "--node", "doc-1", "--content-format", "jsonml", "--tags", "h1"},
|
|
want: "--tags requires --scope tags",
|
|
},
|
|
{
|
|
name: "invalid scope",
|
|
args: []string{"read", "--node", "doc-1", "--content-format", "jsonml", "--scope", "invalid"},
|
|
want: "invalid --scope",
|
|
},
|
|
{
|
|
name: "tags only with tags scope",
|
|
args: []string{"read", "--node", "doc-1", "--content-format", "jsonml", "--scope", "outline", "--tags", "h1"},
|
|
want: "--tags only works",
|
|
},
|
|
{
|
|
name: "tags scope needs tags",
|
|
args: []string{"read", "--node", "doc-1", "--content-format", "jsonml", "--scope", "tags"},
|
|
want: "--tags is required",
|
|
},
|
|
{
|
|
name: "range needs start",
|
|
args: []string{"read", "--node", "doc-1", "--content-format", "jsonml", "--scope", "range"},
|
|
want: "--start-block-id is required",
|
|
},
|
|
{
|
|
name: "section needs start",
|
|
args: []string{"read", "--node", "doc-1", "--content-format", "jsonml", "--scope", "section"},
|
|
want: "--start-block-id is required",
|
|
},
|
|
{
|
|
name: "end only with range",
|
|
args: []string{"read", "--node", "doc-1", "--content-format", "jsonml", "--scope", "outline", "--end-block-id", "end"},
|
|
want: "--end-block-id only works",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
caller := &docReadScopeCaller{}
|
|
err := executeDocReadScopeCommand(t, caller, tt.args...)
|
|
if err == nil || !strings.Contains(err.Error(), tt.want) {
|
|
t.Fatalf("error = %v, want %q", err, tt.want)
|
|
}
|
|
if len(caller.calls) != 0 {
|
|
t.Fatalf("calls = %d, want 0", len(caller.calls))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageDocReadScopeMapsArgumentsAndWritesOutput(t *testing.T) {
|
|
outputPath := filepath.Join(t.TempDir(), "fragment.json")
|
|
caller := &docReadScopeCaller{
|
|
text: `{"jsonml":"[\"fragment\",{\"source\":\"range\"},[\"p\",{},\"body\"]]"}`,
|
|
}
|
|
err := executeDocReadScopeCommand(
|
|
t,
|
|
caller,
|
|
"read", "--node", "doc-1", "--content-format", "jsonml",
|
|
"--scope", "range", "--start-block-id", "start", "--end-block-id", "end",
|
|
"--max-depth", "3", "--output", outputPath,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("execute: %v", err)
|
|
}
|
|
if len(caller.calls) != 1 {
|
|
t.Fatalf("calls = %d, want 1", len(caller.calls))
|
|
}
|
|
call := caller.calls[0]
|
|
wantArgs := map[string]any{
|
|
"nodeId": "doc-1",
|
|
"format": "jsonml",
|
|
"scope": "range",
|
|
"maxDepth": 3,
|
|
"startBlockId": "start",
|
|
"endBlockId": "end",
|
|
}
|
|
if call.server != "doc" || call.tool != "get_document_content" || !reflect.DeepEqual(call.args, wantArgs) {
|
|
t.Fatalf("call = %#v, want doc/get_document_content %#v", call, wantArgs)
|
|
}
|
|
content, err := os.ReadFile(outputPath)
|
|
if err != nil {
|
|
t.Fatalf("read output: %v", err)
|
|
}
|
|
if !strings.Contains(string(content), "\n") || !strings.Contains(string(content), `"fragment"`) {
|
|
t.Fatalf("output was not pretty JSONML fragment: %s", content)
|
|
}
|
|
|
|
t.Run("tags omit unset optional arguments", func(t *testing.T) {
|
|
caller := &docReadScopeCaller{
|
|
text: `{"jsonml":"[\"fragment\",{\"source\":\"tags\"},[\"h1\",{},\"title\"]]"}`,
|
|
}
|
|
output := installDocReadScopeCaller(t, caller)
|
|
root := newDocCommand()
|
|
root.SilenceErrors = true
|
|
root.SilenceUsage = true
|
|
root.SetArgs([]string{
|
|
"read", "--node", "doc-2", "--content-format", "jsonml",
|
|
"--scope", "tags", "--tags", "h1,h2",
|
|
})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatalf("execute: %v", err)
|
|
}
|
|
want := map[string]any{
|
|
"nodeId": "doc-2",
|
|
"format": "jsonml",
|
|
"scope": "tags",
|
|
"tags": "h1,h2",
|
|
}
|
|
if len(caller.calls) != 1 || !reflect.DeepEqual(caller.calls[0].args, want) {
|
|
t.Fatalf("calls = %#v, want %#v", caller.calls, want)
|
|
}
|
|
if !strings.Contains(output.String(), `"fragment"`) {
|
|
t.Fatalf("stdout = %q", output.String())
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCrossPlatformCoverageRunDocReadScopeResponseBranches(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
caller *docReadScopeCaller
|
|
output string
|
|
wantErr string
|
|
wantText string
|
|
}{
|
|
{
|
|
name: "call error",
|
|
caller: &docReadScopeCaller{err: errors.New("read failed")},
|
|
wantErr: "read failed",
|
|
},
|
|
{
|
|
name: "invalid response",
|
|
caller: &docReadScopeCaller{text: `{`},
|
|
wantErr: "failed to parse MCP response",
|
|
},
|
|
{
|
|
name: "missing fragment",
|
|
caller: &docReadScopeCaller{text: `{}`},
|
|
wantText: `"matched": false`,
|
|
},
|
|
{
|
|
name: "invalid fragment is rejected",
|
|
caller: &docReadScopeCaller{text: `{"jsonml":"not-json"}`},
|
|
wantErr: "invalid JSONML fragment",
|
|
},
|
|
{
|
|
name: "write failure",
|
|
caller: &docReadScopeCaller{text: `{"jsonml":"[]"}`},
|
|
output: t.TempDir(),
|
|
wantErr: "failed to write output file",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
output := installDocReadScopeCaller(t, tt.caller)
|
|
err := runDocReadScope("doc-1", "", "", 0, false, "", "ignored", tt.output, "", 0, false)
|
|
if tt.wantErr != "" {
|
|
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
|
t.Fatalf("error = %v, want %q", err, tt.wantErr)
|
|
}
|
|
} else if err != nil {
|
|
t.Fatalf("runDocReadScope: %v", err)
|
|
}
|
|
if tt.wantText != "" && !strings.Contains(output.String(), tt.wantText) {
|
|
t.Fatalf("output = %q, want %q", output.String(), tt.wantText)
|
|
}
|
|
if tt.name == "missing fragment" && !json.Valid(output.Bytes()) {
|
|
t.Fatalf("empty-result stdout is not valid JSON: %q", output.String())
|
|
}
|
|
if len(tt.caller.calls) != 1 {
|
|
t.Fatalf("calls = %d, want 1", len(tt.caller.calls))
|
|
}
|
|
wantMinimal := map[string]any{"nodeId": "doc-1", "format": "jsonml"}
|
|
if !reflect.DeepEqual(tt.caller.calls[0].args, wantMinimal) {
|
|
t.Fatalf("args = %#v, want %#v", tt.caller.calls[0].args, wantMinimal)
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("output receipt is valid JSON", func(t *testing.T) {
|
|
caller := &docReadScopeCaller{text: `{"jsonml":"[\"fragment\",{}]"}`}
|
|
stdout := installDocReadScopeCaller(t, caller)
|
|
outputPath := filepath.Join(t.TempDir(), "fragment.json")
|
|
if err := runDocReadScope("doc-1", "outline", "", 0, false, "", "", outputPath, "", 0, false); err != nil {
|
|
t.Fatalf("runDocReadScope: %v", err)
|
|
}
|
|
var receipt map[string]any
|
|
if err := json.Unmarshal(stdout.Bytes(), &receipt); err != nil {
|
|
t.Fatalf("output receipt is not valid JSON: %q: %v", stdout.String(), err)
|
|
}
|
|
if receipt["success"] != true || receipt["output"] != outputPath {
|
|
t.Fatalf("output receipt = %#v", receipt)
|
|
}
|
|
})
|
|
|
|
t.Run("human output reports missing fragment", func(t *testing.T) {
|
|
caller := &docReadScopeCaller{text: `{}`, format: "raw"}
|
|
stdout := installDocReadScopeCaller(t, caller)
|
|
if err := runDocReadScope("doc-1", "", "", 0, false, "", "", "", "", 0, false); err != nil {
|
|
t.Fatalf("runDocReadScope: %v", err)
|
|
}
|
|
if !strings.Contains(stdout.String(), "未匹配到节点") {
|
|
t.Fatalf("human empty-result output = %q", stdout.String())
|
|
}
|
|
})
|
|
|
|
t.Run("human output reports written fragment", func(t *testing.T) {
|
|
caller := &docReadScopeCaller{text: `{"jsonml":"[\"fragment\",{}]"}`, format: "raw"}
|
|
stdout := installDocReadScopeCaller(t, caller)
|
|
outputPath := filepath.Join(t.TempDir(), "fragment.json")
|
|
if err := runDocReadScope("doc-1", "outline", "", 0, false, "", "", outputPath, "", 0, false); err != nil {
|
|
t.Fatalf("runDocReadScope: %v", err)
|
|
}
|
|
if !strings.Contains(stdout.String(), "JSONML fragment 已写入 "+outputPath) {
|
|
t.Fatalf("human receipt output = %q", stdout.String())
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestCrossPlatformCoverageDocReadAccessParamsForwarding 验证 doc read 三条读取路径
|
|
// 都把 --password / --version 透传到 get_document_content,且非法版本号在
|
|
// 本地校验阶段被拒绝、不产生远端调用。
|
|
func TestCrossPlatformCoverageDocReadAccessParamsForwarding(t *testing.T) {
|
|
t.Run("markdown path forwards password and history version", func(t *testing.T) {
|
|
caller := &docReadScopeCaller{text: `{"markdown":"body"}`}
|
|
if err := executeDocReadScopeCommand(t, caller, "read", "--node", "doc-1", "--password", "pw", "--version", "7"); err != nil {
|
|
t.Fatalf("execute: %v", err)
|
|
}
|
|
if len(caller.calls) != 1 {
|
|
t.Fatalf("calls = %d, want 1", len(caller.calls))
|
|
}
|
|
want := map[string]any{"nodeId": "doc-1", "password": "pw", "historyVersion": 7}
|
|
if caller.calls[0].tool != "get_document_content" || !reflect.DeepEqual(caller.calls[0].args, want) {
|
|
t.Fatalf("call = %#v, want get_document_content %#v", caller.calls[0], want)
|
|
}
|
|
})
|
|
|
|
t.Run("jsonml path forwards password and history version", func(t *testing.T) {
|
|
caller := &docReadScopeCaller{text: `{"jsonml":"[\"root\",{}]","revision":7}`}
|
|
if err := executeDocReadScopeCommand(t, caller, "read", "--node", "doc-1", "--content-format", "jsonml", "--password", "pw", "--version", "7"); err != nil {
|
|
t.Fatalf("execute: %v", err)
|
|
}
|
|
if len(caller.calls) != 1 {
|
|
t.Fatalf("calls = %d, want 1", len(caller.calls))
|
|
}
|
|
want := map[string]any{"nodeId": "doc-1", "format": "jsonml", "password": "pw", "historyVersion": 7}
|
|
if caller.calls[0].tool != "get_document_content" || !reflect.DeepEqual(caller.calls[0].args, want) {
|
|
t.Fatalf("call = %#v, want get_document_content %#v", caller.calls[0], want)
|
|
}
|
|
})
|
|
|
|
t.Run("scope path forwards password and history version", func(t *testing.T) {
|
|
caller := &docReadScopeCaller{text: `{"jsonml":"[\"fragment\",{},[\"h1\",{},\"t\"]]"}`}
|
|
if err := executeDocReadScopeCommand(t, caller,
|
|
"read", "--node", "doc-1", "--content-format", "jsonml", "--scope", "outline",
|
|
"--password", "pw", "--version", "7"); err != nil {
|
|
t.Fatalf("execute: %v", err)
|
|
}
|
|
if len(caller.calls) != 1 {
|
|
t.Fatalf("calls = %d, want 1", len(caller.calls))
|
|
}
|
|
want := map[string]any{"nodeId": "doc-1", "format": "jsonml", "scope": "outline", "password": "pw", "historyVersion": 7}
|
|
if caller.calls[0].tool != "get_document_content" || !reflect.DeepEqual(caller.calls[0].args, want) {
|
|
t.Fatalf("call = %#v, want get_document_content %#v", caller.calls[0], want)
|
|
}
|
|
})
|
|
|
|
t.Run("negative history version is rejected locally", func(t *testing.T) {
|
|
for _, raw := range []string{"-1", "-3"} {
|
|
caller := &docReadScopeCaller{}
|
|
err := executeDocReadScopeCommand(t, caller, "read", "--node", "doc-1", "--version", raw)
|
|
if err == nil || !strings.Contains(err.Error(), "--version 必须为非负整数") {
|
|
t.Fatalf("--version %s error = %v, want invalid --version", raw, err)
|
|
}
|
|
if len(caller.calls) != 0 {
|
|
t.Fatalf("--version %s produced %d remote calls, want 0", raw, len(caller.calls))
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("explicit zero history version forwards initial version", func(t *testing.T) {
|
|
caller := &docReadScopeCaller{text: `{"markdown":"body"}`}
|
|
if err := executeDocReadScopeCommand(t, caller, "read", "--node", "doc-1", "--version", "0"); err != nil {
|
|
t.Fatalf("execute: %v", err)
|
|
}
|
|
if len(caller.calls) != 1 {
|
|
t.Fatalf("calls = %d, want 1", len(caller.calls))
|
|
}
|
|
want := map[string]any{"nodeId": "doc-1", "historyVersion": 0}
|
|
if caller.calls[0].tool != "get_document_content" || !reflect.DeepEqual(caller.calls[0].args, want) {
|
|
t.Fatalf("call = %#v, want get_document_content %#v", caller.calls[0], want)
|
|
}
|
|
})
|
|
|
|
t.Run("unset history version is not forwarded", func(t *testing.T) {
|
|
caller := &docReadScopeCaller{text: `{"markdown":"body"}`}
|
|
if err := executeDocReadScopeCommand(t, caller, "read", "--node", "doc-1"); err != nil {
|
|
t.Fatalf("execute: %v", err)
|
|
}
|
|
if len(caller.calls) != 1 {
|
|
t.Fatalf("calls = %d, want 1", len(caller.calls))
|
|
}
|
|
want := map[string]any{"nodeId": "doc-1"}
|
|
if caller.calls[0].tool != "get_document_content" || !reflect.DeepEqual(caller.calls[0].args, want) {
|
|
t.Fatalf("call = %#v, want get_document_content %#v", caller.calls[0], want)
|
|
}
|
|
})
|
|
}
|