新增审批附件相关dws,预览授权、下载授权、获取下载链接 to #666

This commit is contained in:
昭逸 committed 2026-08-14 17:41:02 +08:00
1 parent ea9e31a59f
commit 6a2e9dd10e
5 files changed
+671 -3

No files matched your search

@@ -0,0 +1,55 @@
# OA Attachment Download URL Output Design
## Goal
Keep the existing command and MCP request unchanged while making the returned
OSS signed URL directly copyable from JSON output:
```text
dws oa approval attachment download-url
```
## Scope
Only `oa approval attachment download-url` changes. The other OA attachment
commands and the global JSON formatter retain their current behavior.
## Design
The command continues to invoke MCP server `oa`, tool
`get_attachment_download_url`, with the same arguments. Its leaf declaration
provides a command-specific `Call` callback that invokes the existing MCP
dispatcher with HTML escaping disabled when the selected output format is
JSON. This preserves literal `&` separators in `result.downloadUri` instead of
rendering them as `\u0026`.
For `raw`, `table`, and other non-JSON formats, the callback uses the existing
escaped dispatcher behavior so their current rendering remains unchanged.
The change does not alter the URL, decode or re-sign it, download the file, or
change global JSON serialization.
## Error Handling
Authentication, MCP transport, gateway, PAT, and business errors continue
through the existing dispatcher and retain their current classification and
output behavior.
## Verification
Add a `TestCrossPlatformCoverage*` regression test that executes the real Cobra
leaf in explicit JSON mode with a fake MCP result containing a signed URL. It
must verify:
- the request still targets `oa/get_attachment_download_url`;
- the exact request arguments remain unchanged, including omission of the
optional boolean when the flag was not supplied;
- stdout contains literal `&OSSAccessKeyId=` and `&Signature=`;
- stdout contains no `\u0026` escape.
The fake caller must report JSON format (or the command must be executed with
`--format json`) so the test fails against the current escaped JSON path rather
than accidentally exercising raw MCP text output.
Run the focused OA attachment tests, format modified Go files, and rebuild the
CLI. No commit is created.
@@ -0,0 +1,103 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package app
import (
"bytes"
"encoding/json"
"strings"
"testing"
)
func TestCrossPlatformCoverageOAAttachmentDeliveredSchemaMatchesExecutableHelp(t *testing.T) {
tests := []struct {
cliPath string
canonical string
rpc string
description string
effect string
}{
{
cliPath: "oa approval attachment download-url",
canonical: "oa.get_attachment_download_url",
rpc: "get_attachment_download_url",
description: "获取审批附件下载授权并生成临时下载链接",
effect: "read",
},
{
cliPath: "oa approval attachment authorize-download",
canonical: "oa.auth_download_file",
rpc: "auth_download_file",
description: "批量授权当前用户下载指定的审批钉盘文件",
effect: "write",
},
{
cliPath: "oa approval attachment authorize-preview",
canonical: "oa.auth_preview_attachment",
rpc: "auth_preview_attachment",
description: "批量授权当前用户预览审批单中的附件",
effect: "write",
},
}
root := NewRootCommand()
for _, test := range tests {
t.Run(test.rpc, func(t *testing.T) {
command := exactCommandForTest(root, test.cliPath)
if command == nil {
t.Fatalf("executable command %q is missing", test.cliPath)
}
var stdout, stderr bytes.Buffer
root.SetOut(&stdout)
root.SetErr(&stderr)
root.SetArgs([]string{"schema", test.cliPath, "--format", "json"})
if err := root.Execute(); err != nil {
t.Fatalf("execute delivery schema leaf: %v; stderr=%s", err, stderr.String())
}
var tool map[string]any
if err := json.Unmarshal(stdout.Bytes(), &tool); err != nil {
t.Fatalf("decode delivery schema leaf: %v", err)
}
if got := schemaContractString(tool["canonical_path"]); got != test.canonical {
t.Fatalf("canonical_path = %q, want %q", got, test.canonical)
}
if got := schemaContractString(tool["primary_cli_path"]); got != test.cliPath {
t.Fatalf("primary_cli_path = %q, want %q", got, test.cliPath)
}
if got := schemaContractString(tool["description"]); !strings.HasPrefix(got, test.description) {
t.Fatalf("description = %q, want prefix %q", got, test.description)
}
if got := schemaContractString(tool["interface_mode"]); got != "mcp" {
t.Fatalf("interface_mode = %q, want mcp", got)
}
if got := schemaContractString(tool["availability"]); got != "available" {
t.Fatalf("availability = %q, want available", got)
}
interfaceRef := schemaInterfaceObject(tool["interface_ref"])
if got := schemaContractString(interfaceRef["product_id"]); got != "oa" {
t.Fatalf("interface_ref.product_id = %q, want oa", got)
}
if got := schemaContractString(interfaceRef["rpc_name"]); got != test.rpc {
t.Fatalf("interface_ref.rpc_name = %q, want %q", got, test.rpc)
}
if got := schemaContractString(tool["effect"]); got != test.effect {
t.Fatalf("effect = %q, want %q", got, test.effect)
}
if got := schemaContractString(tool["risk"]); got != "low" {
t.Fatalf("risk = %q, want low", got)
}
if got := schemaContractString(tool["confirmation"]); got != "not_required" {
t.Fatalf("confirmation = %q, want not_required", got)
}
if got := schemaContractString(tool["idempotency"]); got != "idempotent" {
t.Fatalf("idempotency = %q, want idempotent", got)
}
if problem := schemaHelpFlagCompletenessProblem(test.canonical, test.cliPath, command, tool); problem != "" {
t.Fatal(problem)
}
})
}
}
+6 -3
View File
@@ -49,7 +49,8 @@ func oaFormValues(raw string) ([]map[string]string, error) {
// get_processInstance_records, list_initiated_instances, list_pending_tasks,
// list_user_visible_process, append_task, search_form, oa_ding_user, revert_task,
// get_inst_revert_activities, get_process_schema, forecast_process,
// start_process_instance
// start_process_instance, get_attachment_download_url, auth_download_file,
// auth_preview_attachment
// ──────────────────────────────────────────────────────────
func newOaCommand() *cobra.Command {
@@ -58,9 +59,10 @@ func newOaCommand() *cobra.Command {
contract.RegisterProductDecl(contract.ProductDecl{
ID: "oa",
Selection: contract.ProductSelectionDecl{
AgentSummary: "查询和处理 OA 审批实例、任务、记录、抄送与评论",
AgentSummary: "查询和处理 OA 审批实例、任务、记录、抄送、评论与附件授权",
UseWhen: []string{
"查看待审、已办、已发起或抄送审批,并执行同意、拒绝、撤销、转交等审批动作时",
"获取审批附件下载链接,或为当前用户授权下载、预览审批附件时",
},
AvoidWhen: []string{
"不要用于普通待办任务或工作日志;需要实时监听未来的审批任务/实例事件时使用 event consume",
@@ -70,7 +72,7 @@ func newOaCommand() *cobra.Command {
root := &cobra.Command{
Use: "oa",
Short: "OA 审批 / 同意 / 拒绝 / 撤销",
Long: `管理钉钉 OA 审批:待办查询、审批详情、同意、拒绝、撤销、操作记录、已发起列表、表单列表。`,
Long: `管理钉钉 OA 审批:待办查询、审批详情、同意、拒绝、撤销、操作记录、已发起列表、表单列表与附件授权。`,
RunE: groupRunE,
}
@@ -1376,6 +1378,7 @@ func newOaCommand() *cobra.Command {
approvalForecastCmd,
approvalCreateCmd,
)
approvalCmd.AddCommand(newOAAttachmentCommand())
root.AddCommand(approvalCmd)
return root
+268
View File
@@ -0,0 +1,268 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package helpers
import (
"encoding/json"
"fmt"
"io"
"strings"
"github.com/spf13/cobra"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
)
func parseOAAttachmentFileInfos(raw string) ([]map[string]any, error) {
decoder := json.NewDecoder(strings.NewReader(raw))
decoder.UseNumber()
var items []map[string]json.RawMessage
if err := decoder.Decode(&items); err != nil {
return nil, fmt.Errorf("--file-infos JSON 解析失败: %w", err)
}
if err := rejectTrailingOAAttachmentJSON(decoder); err != nil {
return nil, err
}
if len(items) < 1 || len(items) > 10 {
return nil, fmt.Errorf("--file-infos 必须包含 1 至 10 个文件")
}
infos := make([]map[string]any, 0, len(items))
for index, item := range items {
for name := range item {
if name != "spaceId" && name != "fileId" {
return nil, fmt.Errorf("--file-infos 第 %d 项包含未知字段 %q", index+1, name)
}
}
spaceRaw, ok := item["spaceId"]
if !ok {
return nil, fmt.Errorf("--file-infos 第 %d 项缺少 spaceId", index+1)
}
spaceDecoder := json.NewDecoder(strings.NewReader(string(spaceRaw)))
spaceDecoder.UseNumber()
var spaceValue any
if err := spaceDecoder.Decode(&spaceValue); err != nil {
return nil, fmt.Errorf("--file-infos 第 %d 项 spaceId 不是有效数字: %w", index+1, err)
}
spaceID, ok := spaceValue.(json.Number)
if !ok {
return nil, fmt.Errorf("--file-infos 第 %d 项 spaceId 必须是数字", index+1)
}
fileRaw, ok := item["fileId"]
if !ok {
return nil, fmt.Errorf("--file-infos 第 %d 项缺少 fileId", index+1)
}
var fileID string
if err := json.Unmarshal(fileRaw, &fileID); err != nil {
return nil, fmt.Errorf("--file-infos 第 %d 项 fileId 必须是字符串: %w", index+1, err)
}
fileID = strings.TrimSpace(fileID)
if fileID == "" {
return nil, fmt.Errorf("--file-infos 第 %d 项 fileId 不能为空", index+1)
}
infos = append(infos, map[string]any{"spaceId": spaceID, "fileId": fileID})
}
return infos, nil
}
func rejectTrailingOAAttachmentJSON(decoder *json.Decoder) error {
var extra any
if err := decoder.Decode(&extra); err != io.EOF {
if err == nil {
return fmt.Errorf("--file-infos JSON 包含多余内容")
}
return fmt.Errorf("--file-infos JSON 解析失败: %w", err)
}
return nil
}
func validateOAAttachmentFileInfos(cmd *cobra.Command, _ []string) error {
raw, _ := cmd.Flags().GetString("file-infos")
_, err := parseOAAttachmentFileInfos(raw)
return err
}
func validateOAPreviewFileIDs(cmd *cobra.Command, _ []string) error {
fileIDs, _ := cmd.Flags().GetStringSlice("file-ids")
if len(fileIDs) > 20 {
return fmt.Errorf("--file-ids 最多包含 20 个附件 ID")
}
for index, fileID := range fileIDs {
if strings.TrimSpace(fileID) == "" {
return fmt.Errorf("--file-ids 第 %d 项不能为空", index+1)
}
}
return nil
}
func newOAAttachmentCommand() *cobra.Command {
attachmentCmd := &cobra.Command{
Use: "attachment",
Short: "审批附件授权与下载链接",
RunE: groupRunE,
}
downloadURLCmd := NewLeafCommand(LeafSpec{
Use: "download-url",
Short: "获取审批附件下载链接",
Example: " dws oa approval attachment download-url --instance-id <processInstanceId> --file-id <fileId>",
Server: "oa",
Tool: "get_attachment_download_url",
Call: func(_ *cobra.Command, tool string, args map[string]any) error {
jsonOutput := strings.EqualFold(strings.TrimSpace(deps.Caller.Format()), "json")
return callMCPToolInternalOpts("oa", tool, args, jsonOutput)
},
Flags: []LeafFlag{
{Name: "instance-id", Usage: "审批实例 ID (必填)", Bind: "processInstanceId", Trim: true, Required: true, MarkRequired: true},
{Name: "file-id", Usage: "审批附件文件 ID (必填)", Bind: "fileId", Trim: true, Required: true, MarkRequired: true},
{Name: "with-comment-attachment", Usage: "是否包含评论中的附件", Kind: LeafBool, Bind: "withCommentAttachment"},
},
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "oa",
Name: "get_attachment_download_url",
CanonicalPath: "oa.get_attachment_download_url",
CLIPath: "oa approval attachment download-url",
PrimaryCLIPath: "oa approval attachment download-url",
},
Description: "获取审批附件下载授权并生成临时下载链接",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "oa", RPCName: "get_attachment_download_url"},
},
Selection: contract.SelectionSpec{
AgentSummary: "获取审批实例中指定附件的临时下载链接",
UseWhen: []string{"已从审批详情获得 processInstanceId 和 fileId,需要获取附件下载链接时"},
AvoidWhen: []string{
"只需查看审批表单和附件元数据时使用 dws oa approval detail",
"需要将附件真正保存到本地时不要误认为本命令会下载文件;它只返回链接",
},
Examples: []string{"dws oa approval attachment download-url --instance-id <processInstanceId> --file-id <fileId>"},
},
Parameters: []contract.ParamDecl{
{Name: "instance-id", Property: "processInstanceId", InterfaceType: "string"},
{Name: "file-id", Property: "fileId", InterfaceType: "string"},
{Name: "with-comment-attachment", Property: "withCommentAttachment", InterfaceType: "boolean"},
},
},
})
authorizeDownloadCmd := NewLeafCommand(LeafSpec{
Use: "authorize-download",
Short: "授权当前用户下载审批钉盘文件",
Long: "批量授权当前用户下载指定的审批钉盘文件。",
Example: ` dws oa approval attachment authorize-download --file-infos '[{"spaceId":27827223951,"fileId":"232271651278"}]'`,
Server: "oa",
Tool: "auth_download_file",
Flags: []LeafFlag{
{
Name: "file-infos", Usage: "审批钉盘文件信息 JSON 数组 (必填)",
Bind: "fileInfos", Trim: true, Required: true, MarkRequired: true,
Transform: func(raw string) (any, error) {
return parseOAAttachmentFileInfos(raw)
},
},
},
Constraints: []LeafConstraint{{
Kind: corecmd.Custom, Flags: []string{"file-infos"},
Description: "文件信息列表必须包含 1 至 10 项",
}},
Validate: validateOAAttachmentFileInfos,
Safety: contract.SafetySpec{
Effect: "write", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "oa",
Name: "auth_download_file",
CanonicalPath: "oa.auth_download_file",
CLIPath: "oa approval attachment authorize-download",
PrimaryCLIPath: "oa approval attachment authorize-download",
},
Description: "批量授权当前用户下载指定的审批钉盘文件",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "oa", RPCName: "auth_download_file"},
},
Selection: contract.SelectionSpec{
AgentSummary: "使用审批钉盘 spaceId/fileId 批量授权当前用户下载文件",
UseWhen: []string{"已有一个或多个审批钉盘文件的 spaceId 和 fileId,需要为当前用户取得下载权限时"},
AvoidWhen: []string{
"需要生成单个审批附件下载链接时使用 attachment download-url",
"需要授权在审批单内预览附件时使用 attachment authorize-preview",
},
Examples: []string{`dws oa approval attachment authorize-download --file-infos '[{"spaceId":27827223951,"fileId":"232271651278"}]'`},
},
Parameters: []contract.ParamDecl{
{Name: "file-infos", Property: "fileInfos", InterfaceType: "array"},
},
},
})
authorizePreviewCmd := NewLeafCommand(LeafSpec{
Use: "authorize-preview",
Short: "授权当前用户预览审批附件",
Long: "批量授权当前用户预览审批单中的附件。",
Example: " dws oa approval attachment authorize-preview --instance-id <processInstanceId> --file-ids <fileId1>,<fileId2>",
Server: "oa",
Tool: "auth_preview_attachment",
Flags: []LeafFlag{
{Name: "instance-id", Usage: "审批实例 ID (必填)", Bind: "processInstanceId", Trim: true, Required: true, MarkRequired: true},
{Name: "file-ids", Usage: "附件 ID 列表,多个用逗号分隔 (必填)", Kind: LeafStringSlice, Bind: "fileIdList", Required: true, MarkRequired: true},
{Name: "with-comment-attachment", Usage: "是否包含评论中的附件", Kind: LeafBool, Bind: "withCommentAttachment"},
},
Constraints: []LeafConstraint{{
Kind: corecmd.Custom, Flags: []string{"file-ids"},
Description: "附件 ID 列表最多包含 20 项且每项不能为空",
}},
Validate: validateOAPreviewFileIDs,
Safety: contract.SafetySpec{
Effect: "write", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "oa",
Name: "auth_preview_attachment",
CanonicalPath: "oa.auth_preview_attachment",
CLIPath: "oa approval attachment authorize-preview",
PrimaryCLIPath: "oa approval attachment authorize-preview",
},
Description: "批量授权当前用户预览审批单中的附件",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "oa", RPCName: "auth_preview_attachment"},
},
Selection: contract.SelectionSpec{
AgentSummary: "按审批实例和附件 ID 列表授权当前用户预览附件",
UseWhen: []string{"已有 processInstanceId 和附件 fileId 列表,需要在审批场景中批量取得预览权限时"},
AvoidWhen: []string{
"需要下载权限而不是预览权限时使用 attachment authorize-download",
"需要直接获得单个附件临时下载链接时使用 attachment download-url",
},
Examples: []string{"dws oa approval attachment authorize-preview --instance-id <processInstanceId> --file-ids <fileId1>,<fileId2>"},
},
Parameters: []contract.ParamDecl{
{Name: "instance-id", Property: "processInstanceId", InterfaceType: "string"},
{Name: "file-ids", Property: "fileIdList", InterfaceType: "array"},
{Name: "with-comment-attachment", Property: "withCommentAttachment", InterfaceType: "boolean"},
},
},
})
attachmentCmd.AddCommand(downloadURLCmd, authorizeDownloadCmd, authorizePreviewCmd)
return attachmentCmd
}
+239
View File
@@ -0,0 +1,239 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package helpers
import (
"bytes"
"encoding/json"
"io"
"os"
"reflect"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam"
)
func executeOAAttachmentCommandCapturingOutput(t *testing.T, caller *scriptedToolCaller, args ...string) (string, error) {
t.Helper()
testseam.Protect(t, &deps)
testseam.Swap(t, &os.Args, []string{"dws", "oa"})
InitDeps(caller)
var stdout bytes.Buffer
deps.Out.w = &stdout
deps.Out.errW = io.Discard
cmd := newOaCommand()
cmd.PersistentFlags().Bool("yes", false, "跳过确认")
cmd.SilenceErrors = true
cmd.SilenceUsage = true
cmd.SetOut(&stdout)
cmd.SetErr(io.Discard)
cmd.SetArgs(args)
err := cmd.Execute()
return stdout.String(), err
}
func TestCrossPlatformCoverageOAAttachmentDownloadURLPreservesSignedQuery(t *testing.T) {
const response = `{"result":{"downloadUri":"https://example.test/file?Expires=1&OSSAccessKeyId=2&Signature=3"},"success":true}`
commandArgs := []string{
"approval", "attachment", "download-url",
"--instance-id", "instance-1",
"--file-id", "file-1",
}
wantArgs := map[string]any{
"processInstanceId": "instance-1",
"fileId": "file-1",
}
t.Run("json", func(t *testing.T) {
caller := &scriptedToolCaller{format: "json", steps: []scriptedToolStep{{text: response}}}
stdout, err := executeOAAttachmentCommandCapturingOutput(t, caller, commandArgs...)
if err != nil {
t.Fatalf("execute command: %v", err)
}
if caller.server != "oa" || caller.tool != "get_attachment_download_url" {
t.Fatalf("called %s/%s, want oa/get_attachment_download_url", caller.server, caller.tool)
}
if !reflect.DeepEqual(caller.args, wantArgs) {
t.Fatalf("args = %#v, want %#v", caller.args, wantArgs)
}
if !strings.Contains(stdout, "&OSSAccessKeyId=") || !strings.Contains(stdout, "&Signature=") {
t.Fatalf("stdout does not preserve signed query separators: %q", stdout)
}
if strings.Contains(stdout, `\u0026`) {
t.Fatalf("stdout contains escaped query separator: %q", stdout)
}
})
t.Run("raw compatibility", func(t *testing.T) {
caller := &scriptedToolCaller{format: "raw", steps: []scriptedToolStep{{text: response}}}
stdout, err := executeOAAttachmentCommandCapturingOutput(t, caller, commandArgs...)
if err != nil {
t.Fatalf("execute command: %v", err)
}
if stdout != response+"\n" {
t.Fatalf("stdout = %q, want %q", stdout, response+"\n")
}
})
}
func TestCrossPlatformCoverageOAAttachmentPayloads(t *testing.T) {
tests := []struct {
name string
args []string
wantTool string
check func(*testing.T, map[string]any)
}{
{
name: "download URL",
args: []string{
"approval", "attachment", "download-url",
"--instance-id", "instance-1",
"--file-id", "file-1",
"--with-comment-attachment=false",
},
wantTool: "get_attachment_download_url",
check: func(t *testing.T, got map[string]any) {
t.Helper()
if got["processInstanceId"] != "instance-1" || got["fileId"] != "file-1" {
t.Fatalf("download identifiers = %#v", got)
}
if value, ok := got["withCommentAttachment"].(bool); !ok || value {
t.Fatalf("withCommentAttachment = %#v, want explicit false", got["withCommentAttachment"])
}
},
},
{
name: "download authorization",
args: []string{
"approval", "attachment", "authorize-download",
"--file-infos", `[{"spaceId":27827223951,"fileId":"file-2"}]`,
},
wantTool: "auth_download_file",
check: func(t *testing.T, got map[string]any) {
t.Helper()
infos, ok := got["fileInfos"].([]map[string]any)
if !ok || len(infos) != 1 {
t.Fatalf("fileInfos = %#v", got["fileInfos"])
}
if infos[0]["spaceId"] != json.Number("27827223951") || infos[0]["fileId"] != "file-2" {
t.Fatalf("fileInfos[0] = %#v", infos[0])
}
},
},
{
name: "preview authorization",
args: []string{
"approval", "attachment", "authorize-preview",
"--instance-id", "instance-2",
"--file-ids", "file-3,file-4",
"--with-comment-attachment",
},
wantTool: "auth_preview_attachment",
check: func(t *testing.T, got map[string]any) {
t.Helper()
if got["processInstanceId"] != "instance-2" || got["withCommentAttachment"] != true {
t.Fatalf("preview scalar payload = %#v", got)
}
ids, ok := got["fileIdList"].([]string)
if !ok || len(ids) != 2 || ids[0] != "file-3" || ids[1] != "file-4" {
t.Fatalf("fileIdList = %#v", got["fileIdList"])
}
},
},
{
name: "optional boolean omitted",
args: []string{
"approval", "attachment", "download-url",
"--instance-id", "instance-3",
"--file-id", "file-5",
},
wantTool: "get_attachment_download_url",
check: func(t *testing.T, got map[string]any) {
t.Helper()
if _, exists := got["withCommentAttachment"]; exists {
t.Fatalf("optional withCommentAttachment should be omitted: %#v", got)
}
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
caller := &scriptedToolCaller{}
if err := executeOACommand(t, caller, test.args...); err != nil {
t.Fatalf("execute command: %v", err)
}
if caller.server != "oa" || caller.tool != test.wantTool {
t.Fatalf("called %s/%s, want oa/%s", caller.server, caller.tool, test.wantTool)
}
test.check(t, caller.args)
})
}
}
func TestCrossPlatformCoverageOAAttachmentRejectsInvalidFileInfos(t *testing.T) {
valid := `{"spaceId":27827223951,"fileId":"file-1"}`
tests := []struct {
name string
raw string
}{
{name: "malformed JSON", raw: `[{`},
{name: "null", raw: `null`},
{name: "not array", raw: `{}`},
{name: "empty array", raw: `[]`},
{name: "more than ten", raw: `[` + strings.Repeat(valid+`,`, 10) + valid + `]`},
{name: "missing space ID", raw: `[{"fileId":"file-1"}]`},
{name: "string space ID", raw: `[{"spaceId":"27827223951","fileId":"file-1"}]`},
{name: "missing file ID", raw: `[{"spaceId":27827223951}]`},
{name: "blank file ID", raw: `[{"spaceId":27827223951,"fileId":" "}]`},
{name: "unknown property", raw: `[{"spaceId":27827223951,"fileId":"file-1","extra":true}]`},
{name: "trailing JSON", raw: `[` + valid + `] {}`},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
caller := &scriptedToolCaller{}
err := executeOACommand(t, caller,
"approval", "attachment", "authorize-download",
"--file-infos", test.raw,
)
if err == nil {
t.Fatalf("fileInfos %q unexpectedly succeeded", test.raw)
}
if caller.calls != 0 {
t.Fatalf("invalid fileInfos made %d MCP call(s)", caller.calls)
}
})
}
}
func TestCrossPlatformCoverageOAAttachmentRejectsInvalidPreviewFileIDs(t *testing.T) {
tests := []struct {
name string
raw string
}{
{name: "separator only", raw: `,`},
{name: "blank item", raw: `file-1, ,file-2`},
{name: "more than twenty", raw: strings.Repeat("file,", 20) + "file"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
caller := &scriptedToolCaller{}
err := executeOACommand(t, caller,
"approval", "attachment", "authorize-preview",
"--instance-id", "instance-1",
"--file-ids", test.raw,
)
if err == nil {
t.Fatalf("file IDs %q unexpectedly succeeded", test.raw)
}
if caller.calls != 0 {
t.Fatalf("invalid file IDs made %d MCP call(s)", caller.calls)
}
})
}
}