767 lines
31 KiB
Go
767 lines
31 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
package helpers
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/open-dingtalk/dingtalk-stream-sdk-go/chatbot"
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
|
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam"
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
|
|
)
|
|
|
|
type coverageFailingReader struct{}
|
|
|
|
func (coverageFailingReader) Read([]byte) (int, error) { return 0, errors.New("read failed") }
|
|
|
|
type coverageMediaClient struct {
|
|
downloadPicture func(string) (string, error)
|
|
downloadNamed func(fileInboundInfo) (string, error)
|
|
downloadRecover func(fileInboundInfo) (string, error)
|
|
unionID func(string) (string, error)
|
|
downloadDentry func(fileInboundInfo, string) (string, error)
|
|
}
|
|
|
|
type coverageStreamingAttachmentForwarder struct{ called bool }
|
|
|
|
func (*coverageStreamingAttachmentForwarder) label() string { return "streaming-attachment" }
|
|
func (*coverageStreamingAttachmentForwarder) forward(context.Context, string, string) (string, error) {
|
|
return "plain", nil
|
|
}
|
|
|
|
type coverageErrorCaller struct {
|
|
err error
|
|
result *edition.ToolResult
|
|
dryRun bool
|
|
}
|
|
|
|
func (c *coverageErrorCaller) CallTool(context.Context, string, string, map[string]any) (*edition.ToolResult, error) {
|
|
return c.result, c.err
|
|
}
|
|
func (*coverageErrorCaller) Format() string { return "json" }
|
|
func (c *coverageErrorCaller) DryRun() bool { return c.dryRun }
|
|
func (*coverageErrorCaller) Fields() string { return "" }
|
|
func (*coverageErrorCaller) JQ() string { return "" }
|
|
func (*coverageStreamingAttachmentForwarder) forwardWithAttachments(context.Context, string, string, []connectMediaAttachment) (string, error) {
|
|
return "attachment", nil
|
|
}
|
|
func (*coverageStreamingAttachmentForwarder) canStream() bool { return true }
|
|
func (f *coverageStreamingAttachmentForwarder) forwardStreamWithAttachments(_ context.Context, _, _ string, _ []connectMediaAttachment, onDelta func(string)) (string, error) {
|
|
f.called = true
|
|
if onDelta != nil {
|
|
onDelta("delta")
|
|
}
|
|
return "streamed", nil
|
|
}
|
|
|
|
func (c *coverageMediaClient) downloadMessageFile(_ context.Context, _, code string) (string, error) {
|
|
return c.downloadPicture(code)
|
|
}
|
|
|
|
func (c *coverageMediaClient) downloadMessageFileNamed(_ context.Context, _, code, name string) (string, error) {
|
|
return c.downloadNamed(fileInboundInfo{DownloadCode: code, FileName: name})
|
|
}
|
|
|
|
func (c *coverageMediaClient) downloadRecoveredChatRecordFile(_ context.Context, info fileInboundInfo) (string, error) {
|
|
return c.downloadRecover(info)
|
|
}
|
|
|
|
func (c *coverageMediaClient) getUserUnionID(_ context.Context, userID string) (string, error) {
|
|
return c.unionID(userID)
|
|
}
|
|
|
|
func (c *coverageMediaClient) downloadDentryFile(_ context.Context, spaceID, dentryID int64, unionID, name string) (string, error) {
|
|
return c.downloadDentry(fileInboundInfo{SpaceID: spaceID, DentryID: dentryID, FileName: name}, unionID)
|
|
}
|
|
|
|
func TestCrossPlatformCoverageConnectMediaParsingRemainingEdges(t *testing.T) {
|
|
empty := filepath.Join(t.TempDir(), "empty.bin")
|
|
if err := os.WriteFile(empty, nil, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := connectAttachmentMIME(empty); got != "application/octet-stream" {
|
|
t.Fatalf("empty MIME = %q", got)
|
|
}
|
|
|
|
pictures, files := richTextInboundMedia(map[string]any{"richText": []any{"not-an-object"}})
|
|
if len(pictures) != 0 || len(files) != 0 {
|
|
t.Fatalf("non-object rich text = %v, %v", pictures, files)
|
|
}
|
|
pictures, files, unknown := callbackInboundMedia("richText", map[string]any{"richText": []any{
|
|
map[string]any{"type": "picture", "pictureDownloadCode": "inline-picture"},
|
|
}})
|
|
if len(pictures) != 1 || pictures[0] != "inline-picture" || len(files) != 0 || unknown != 0 {
|
|
t.Fatalf("rich callback media = %v, %v, %d", pictures, files, unknown)
|
|
}
|
|
if got := parseTypedFileInbound("video", map[string]any{"downloadCode": "v"}); got.FileName != "视频消息" {
|
|
t.Fatalf("video fallback = %#v", got)
|
|
}
|
|
for _, tc := range []struct {
|
|
info fileInboundInfo
|
|
want string
|
|
}{
|
|
{fileInboundInfo{MediaID: "m", OpenMessageID: "msg", OpenConversationID: "conv"}, "media:m:msg:conv"},
|
|
{fileInboundInfo{FileID: "f"}, "file:f"},
|
|
{fileInboundInfo{DentryID: 2, SpaceID: 1}, "dentry:1:2"},
|
|
{fileInboundInfo{}, ""},
|
|
} {
|
|
if got := fileInboundKey(tc.info); got != tc.want {
|
|
t.Fatalf("fileInboundKey(%#v) = %q", tc.info, got)
|
|
}
|
|
}
|
|
|
|
if p, f, n := chatRecordInboundMedia(map[string]any{"chatRecord": "{"}); len(p) != 0 || len(f) != 0 || n != 0 {
|
|
t.Fatalf("bad chat record = %v, %v, %d", p, f, n)
|
|
}
|
|
content := map[string]any{"chatRecord": []any{
|
|
"not-an-object",
|
|
map[string]any{"msgType": "file", "downloadCode": "dup", "fileName": "a.txt"},
|
|
map[string]any{"msgType": "file", "downloadCode": "dup", "fileName": "a.txt"},
|
|
}}
|
|
_, files, _ = chatRecordInboundMedia(content)
|
|
if len(files) != 1 {
|
|
t.Fatalf("decoded chat record files = %#v", files)
|
|
}
|
|
if !hasChatRecordPayload(map[string]any{"chatRecord": []any{}}) {
|
|
t.Fatal("decoded chatRecord payload was not detected")
|
|
}
|
|
|
|
pictures, files, _ = callbackInboundMedia("picture", map[string]any{
|
|
"pictureDownloadCode": "same",
|
|
"downloadCode": "same",
|
|
"fileName": "same.png",
|
|
"chatRecord": []any{
|
|
map[string]any{"msgType": "picture", "pictureDownloadCode": "same"},
|
|
map[string]any{"msgType": "file", "downloadCode": "file", "fileName": "a.txt"},
|
|
map[string]any{"msgType": "file", "downloadCode": "file", "fileName": "a.txt"},
|
|
},
|
|
})
|
|
if len(pictures) != 1 || len(files) != 1 {
|
|
t.Fatalf("deduplicated callback media = %v, %#v", pictures, files)
|
|
}
|
|
if got := rawCallbackPrompt("custom", make(chan int)); !strings.Contains(got, "msgtype=\"custom\"") {
|
|
t.Fatalf("raw callback fallback = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageWriteCompleteMediaFileRemainingEdges(t *testing.T) {
|
|
origCreate, origCopy, origRemove := mediaCreate, mediaCopy, mediaRemove
|
|
t.Cleanup(func() {
|
|
mediaCreate, mediaCopy, mediaRemove = origCreate, origCopy, origRemove
|
|
})
|
|
|
|
removed := 0
|
|
mediaRemove = func(string) error { removed++; return nil }
|
|
mediaCopy = func(io.Writer, io.Reader) (int64, error) { return 0, nil }
|
|
mediaCreate = func(string) (*os.File, error) {
|
|
f, err := os.CreateTemp(t.TempDir(), "closed")
|
|
if err == nil {
|
|
err = f.Close()
|
|
}
|
|
return f, err
|
|
}
|
|
if err := writeCompleteMediaFile("unused", strings.NewReader("x")); err == nil {
|
|
t.Fatal("close error expected")
|
|
}
|
|
mediaCreate = origCreate
|
|
mediaCopy = func(io.Writer, io.Reader) (int64, error) { return mediaMaxDownloadBytes + 1, nil }
|
|
if err := writeCompleteMediaFile(filepath.Join(t.TempDir(), "large"), strings.NewReader("x")); err == nil {
|
|
t.Fatal("oversized copy expected")
|
|
}
|
|
if removed < 2 {
|
|
t.Fatalf("remove calls = %d", removed)
|
|
}
|
|
|
|
root := filepath.Join(os.TempDir(), "dws-connect-media")
|
|
if err := os.MkdirAll(root, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
path := filepath.Join(root, "cleanup-test")
|
|
if err := os.WriteFile(path, []byte("x"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mediaRemove = origRemove
|
|
cleanupConnectMediaAttachments([]connectMediaAttachment{{LocalPath: path}})
|
|
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
|
t.Fatalf("cleanup stat = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoveragePivotAndInputValidationRemainingEdges(t *testing.T) {
|
|
cases := []map[string]any{
|
|
{"values": "not-array"},
|
|
{"values": []any{map[string]any{}}},
|
|
{"values": []any{map[string]any{"field": "x", "summarize_by": 1}}},
|
|
{"rows": "not-array"},
|
|
{"rows": []any{"not-object"}},
|
|
{"collapse": "invalid"},
|
|
}
|
|
for i, props := range cases {
|
|
if err := validatePivotTableProperties(props, false); err == nil {
|
|
t.Fatalf("pivot case %d succeeded: %#v", i, props)
|
|
}
|
|
}
|
|
if _, err := validatePivotField("bad", "field"); err == nil {
|
|
t.Fatal("non-object pivot field succeeded")
|
|
}
|
|
if _, err := validatePivotField(map[string]any{"field": 1}, "field"); err == nil {
|
|
t.Fatal("non-string pivot field succeeded")
|
|
}
|
|
if _, err := readPivotProperties("@"+filepath.Join(t.TempDir(), "missing"), false); err == nil {
|
|
t.Fatal("missing pivot properties file succeeded")
|
|
}
|
|
|
|
cmd := &cobra.Command{}
|
|
cmd.SetIn(coverageFailingReader{})
|
|
cmd.Flags().String("table", "-", "")
|
|
if _, err := readTableJSONFlag(cmd, "table"); err == nil {
|
|
t.Fatal("stdin read failure succeeded")
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageDangerousConfirmationRemainingEdges(t *testing.T) {
|
|
if confirmDangerousAction(nil, "publish", "resource") {
|
|
t.Fatal("nil command confirmed")
|
|
}
|
|
cmd := &cobra.Command{}
|
|
cmd.Flags().Bool("yes", false, "")
|
|
cmd.SetIn(strings.NewReader("y\n"))
|
|
if !confirmDangerousAction(cmd, "publish", "resource") {
|
|
t.Fatal("interactive y was rejected")
|
|
}
|
|
cmd = &cobra.Command{}
|
|
cmd.Flags().Bool("yes", false, "")
|
|
cmd.SetIn(strings.NewReader("no\n"))
|
|
if confirmDangerousAction(cmd, "publish", "resource") {
|
|
t.Fatal("interactive no was accepted")
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageProtectSheetMutationCommandPanics(t *testing.T) {
|
|
assertPanic := func(name string, fn func()) {
|
|
t.Helper()
|
|
defer func() {
|
|
if recover() == nil {
|
|
t.Errorf("%s did not panic", name)
|
|
}
|
|
}()
|
|
fn()
|
|
}
|
|
assertPanic("nil", func() { protectSheetMutationCommand(nil, "delete", "target") })
|
|
assertPanic("duplicate", func() {
|
|
protectSheetMutationCommand(&cobra.Command{Annotations: map[string]string{sheetMutationConfirmationGuardAnnotation: "true"}, RunE: func(*cobra.Command, []string) error { return nil }}, "delete", "target")
|
|
})
|
|
assertPanic("missing RunE", func() { protectSheetMutationCommand(&cobra.Command{Use: "leaf"}, "delete", "target") })
|
|
}
|
|
|
|
func TestSheetMutationGuardRejectsPipedYesEvenWithContractConfirmSafety(t *testing.T) {
|
|
// Sheet agent hardening: outer --yes-only gate must win over ConfirmSafety
|
|
// honoring piped stdin yes (review: delete-sheet / range clear / version revert).
|
|
// This fixture exercises the no-caller fallback. Isolate it from commands
|
|
// built by earlier tests, which may initialize the package-level deps.
|
|
testseam.Protect(t, &deps)
|
|
deps = nil
|
|
ran := false
|
|
cmd := &cobra.Command{
|
|
Use: "delete-sheet",
|
|
RunE: func(*cobra.Command, []string) error {
|
|
ran = true
|
|
return nil
|
|
},
|
|
}
|
|
cmd.Flags().Bool("yes", false, "")
|
|
cmd.Flags().Bool("dry-run", false, "")
|
|
cmd.Flags().String("node", "", "")
|
|
DeclareLeafMetadata(cmd, LeafSpec{
|
|
Safety: contract.SafetySpec{
|
|
Effect: "destructive", Risk: "high",
|
|
Confirmation: "user_required", Idempotency: "unknown",
|
|
},
|
|
Contract: LeafContract{
|
|
Identity: contract.ToolIdentitySpec{
|
|
ProductID: "dev",
|
|
Name: "create_thing",
|
|
CanonicalPath: "dev.create_thing",
|
|
CLIPath: "dev create",
|
|
PrimaryCLIPath: "dev create",
|
|
},
|
|
|
|
Description: "test sheet delete",
|
|
Interface: &contract.InterfaceSpec{
|
|
Mode: "composite",
|
|
Availability: "available",
|
|
Reason: "unit test fixture",
|
|
},
|
|
Selection: contract.SelectionSpec{
|
|
AgentSummary: "test sheet delete",
|
|
UseWhen: []string{"unit test"},
|
|
AvoidWhen: []string{"unit test"},
|
|
Examples: []string{"dws sheet delete-sheet --yes"},
|
|
},
|
|
},
|
|
})
|
|
protectSheetMutationCommand(cmd, "删除工作表", "文档和工作表")
|
|
if !HasContractConfirmSafety(cmd) || !HasSheetMutationConfirmationGuard(cmd) {
|
|
t.Fatal("expected both contract confirm and sheet mutation markers")
|
|
}
|
|
|
|
// Missing --node must fail before confirmation (RFC §5.1).
|
|
cmd.SetArgs(nil)
|
|
err := cmd.Execute()
|
|
if err == nil || !strings.Contains(err.Error(), "flag --node is required") {
|
|
t.Fatalf("missing --node must validate first, got %#v", err)
|
|
}
|
|
if ran {
|
|
t.Fatal("RunE must not run when --node is missing")
|
|
}
|
|
|
|
if err := cmd.Flags().Set("node", "node-probe"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cmd.SetIn(strings.NewReader("yes\n"))
|
|
cmd.SetArgs(nil)
|
|
err = cmd.Execute()
|
|
var appErr *apperrors.Error
|
|
if !errors.As(err, &appErr) || appErr.Reason != "confirmation_required" {
|
|
t.Fatalf("piped yes must be confirmation_required, got %#v", err)
|
|
}
|
|
if ran {
|
|
t.Fatal("RunE must not run without --yes")
|
|
}
|
|
|
|
ran = false
|
|
cmd.SetIn(strings.NewReader(""))
|
|
if err := cmd.Flags().Set("yes", "true"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := cmd.Execute(); err != nil {
|
|
t.Fatalf("--yes must proceed, got %v", err)
|
|
}
|
|
if !ran {
|
|
t.Fatal("RunE must run with --yes")
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageOpenCodeAttachmentFallbackNameAndStat(t *testing.T) {
|
|
orig := generateConnectVideoStoryboard
|
|
t.Cleanup(func() { generateConnectVideoStoryboard = orig })
|
|
video := filepath.Join(t.TempDir(), "video.mp4")
|
|
if err := os.WriteFile(video, []byte("video"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
storyboard := filepath.Join(t.TempDir(), "storyboard.jpg")
|
|
generateConnectVideoStoryboard = func(context.Context, string) (string, error) { return storyboard, nil }
|
|
prompt, attachments := prepareOpenCodeAttachments(context.Background(), video, []connectMediaAttachment{{LocalPath: video, MediaType: "video"}})
|
|
if len(attachments) != 1 || attachments[0].FileName != "转发视频.storyboard.jpg" || !strings.Contains(prompt, storyboard) {
|
|
t.Fatalf("prepared = %q, %#v", prompt, attachments)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageAssembleConnectTurnMediaBranches(t *testing.T) {
|
|
origRecover := recoverChatRecordUnknownsForConnect
|
|
t.Cleanup(func() { recoverChatRecordUnknownsForConnect = origRecover })
|
|
fail := errors.New("download failed")
|
|
client := &coverageMediaClient{
|
|
downloadPicture: func(code string) (string, error) {
|
|
if strings.HasPrefix(code, "ok") {
|
|
return "/tmp/" + code + ".png", nil
|
|
}
|
|
return "", fail
|
|
},
|
|
downloadNamed: func(info fileInboundInfo) (string, error) {
|
|
if strings.HasPrefix(info.DownloadCode, "ok") {
|
|
return "/tmp/" + info.FileName, nil
|
|
}
|
|
return "", fail
|
|
},
|
|
downloadRecover: func(info fileInboundInfo) (string, error) {
|
|
if info.MediaID == "ok" || info.FileID == "ok" {
|
|
return "/tmp/" + info.FileName, nil
|
|
}
|
|
return "", fail
|
|
},
|
|
unionID: func(userID string) (string, error) {
|
|
if userID == "union-fail" {
|
|
return "", fail
|
|
}
|
|
return "union", nil
|
|
},
|
|
downloadDentry: func(info fileInboundInfo, _ string) (string, error) {
|
|
switch info.DentryID {
|
|
case 1:
|
|
return "/tmp/" + info.FileName, nil
|
|
case 2:
|
|
return "", fail
|
|
default:
|
|
return "", nil
|
|
}
|
|
},
|
|
}
|
|
|
|
recoverChatRecordUnknownsForConnect = func(context.Context, chatRecordLookup, chatRecordToolCall) (chatRecordEnrichment, error) {
|
|
return chatRecordEnrichment{}, fail
|
|
}
|
|
prompt, attachments := assembleConnectTurnMedia("", "client", "sender", client, []string{"bad"}, nil, []chatRecordLookup{{MsgID: "bad"}})
|
|
if prompt == "" || len(attachments) != 0 {
|
|
t.Fatalf("failed recovery/picture = %q, %#v", prompt, attachments)
|
|
}
|
|
assembleConnectTurnMedia("text", "client", "sender", client, []string{"bad"}, nil, nil)
|
|
|
|
recoverChatRecordUnknownsForConnect = func(context.Context, chatRecordLookup, chatRecordToolCall) (chatRecordEnrichment, error) {
|
|
return chatRecordEnrichment{Prompt: "recovered", Files: []fileInboundInfo{{DownloadCode: "ok-file", FileName: "recovered.txt"}}, MissingCount: 1}, nil
|
|
}
|
|
prompt, attachments = assembleConnectTurnMedia("", "client", "sender", client, []string{"ok-picture"}, []fileInboundInfo{{}}, []chatRecordLookup{{MsgID: "ok"}})
|
|
if !strings.Contains(prompt, "recovered") || len(attachments) != 2 {
|
|
t.Fatalf("successful recovery = %q, %#v", prompt, attachments)
|
|
}
|
|
assembleConnectTurnMedia("existing", "client", "sender", client, nil, nil, []chatRecordLookup{{MsgID: "ok"}})
|
|
assembleConnectTurnMedia("", "client", "sender", client, []string{"ok-picture"}, nil, nil)
|
|
|
|
files := []fileInboundInfo{
|
|
{DownloadCode: "ok-image", FileName: "image.png", MediaType: "image"},
|
|
{DownloadCode: "ok-audio", FileName: "audio.mp3", MediaType: "audio"},
|
|
{DownloadCode: "ok-video", FileName: "video.mp4", MediaType: "video"},
|
|
{DownloadCode: "bad", FileName: "failed.txt", MediaType: "file"},
|
|
{MediaID: "ok", OpenMessageID: "message", OpenConversationID: "conversation", FileName: "recovered.bin", MediaType: "file"},
|
|
{FileID: "bad", FileName: "missing.bin", MediaType: "file"},
|
|
}
|
|
prompt, attachments = assembleConnectTurnMedia("", "client", "sender", client, nil, files, nil)
|
|
if prompt == "" || len(attachments) != 4 {
|
|
t.Fatalf("file variants = %q, %#v", prompt, attachments)
|
|
}
|
|
|
|
dentry := func(id int64) fileInboundInfo {
|
|
return fileInboundInfo{DentryID: id, SpaceID: 9, FileName: "disk.txt", FileType: "text", FileSize: 5}
|
|
}
|
|
prompt, attachments = assembleConnectTurnMedia("", "client", "sender", client, nil, []fileInboundInfo{dentry(1)}, nil)
|
|
if prompt == "" || len(attachments) != 1 {
|
|
t.Fatalf("dentry success = %q, %#v", prompt, attachments)
|
|
}
|
|
assembleConnectTurnMedia("existing", "client", "sender", client, nil, []fileInboundInfo{dentry(1)}, nil)
|
|
for _, sender := range []string{"union-fail", "sender"} {
|
|
for _, initial := range []string{"", "existing"} {
|
|
prompt, attachments = assembleConnectTurnMedia(initial, "client", sender, client, nil, []fileInboundInfo{dentry(2)}, nil)
|
|
if prompt == "" || len(attachments) != 0 {
|
|
t.Fatalf("dentry fallback sender=%q initial=%q: %q, %#v", sender, initial, prompt, attachments)
|
|
}
|
|
}
|
|
}
|
|
for _, initial := range []string{"", "existing"} {
|
|
prompt, attachments = assembleConnectTurnMedia(initial, "client", "sender", client, nil, []fileInboundInfo{{DownloadCode: "bad", FileName: "lost.txt"}}, nil)
|
|
if prompt == "" || len(attachments) != 0 {
|
|
t.Fatalf("ordinary fallback = %q, %#v", prompt, attachments)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageChatRecordDownloadWrapperAndWriteEdges(t *testing.T) {
|
|
origCall := chatRecordDownloadToolCall
|
|
origCreate := mediaCreate
|
|
t.Cleanup(func() {
|
|
chatRecordDownloadToolCall = origCall
|
|
mediaCreate = origCreate
|
|
})
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.Header().Set("Content-Type", "image/png")
|
|
_, _ = w.Write([]byte("image"))
|
|
}))
|
|
defer server.Close()
|
|
chatRecordDownloadToolCall = func(context.Context, string, string, map[string]any) (string, error) {
|
|
return `{"downloadUrl":"` + server.URL + `"}`, nil
|
|
}
|
|
client := &aiCardClient{httpClient: server.Client()}
|
|
path, err := client.downloadRecoveredChatRecordFile(context.Background(), fileInboundInfo{FileID: "file"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.Remove(path)
|
|
if filepath.Ext(path) != ".png" {
|
|
t.Fatalf("download extension = %q", path)
|
|
}
|
|
|
|
mediaCreate = func(string) (*os.File, error) { return nil, errors.New("create failed") }
|
|
if _, err := downloadConnectURLToTemp(context.Background(), server.Client(), server.URL, nil, "file.txt"); err == nil {
|
|
t.Fatal("media create failure succeeded")
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageChatRecordEnrichmentInvalidConversationResponse(t *testing.T) {
|
|
resolved := map[string]chatRecordMessage{"wanted": {OpenMessageID: "wanted", Content: "[文件] old"}}
|
|
all := []chatRecordMessage{{OpenMessageID: "wanted", OpenConversationID: "conv", CreateTime: "2026-01-01 00:00:00"}}
|
|
enrichForwardedFileLocators(context.Background(), all, resolved, func(context.Context, string, string, map[string]any) (string, error) {
|
|
return "invalid", nil
|
|
})
|
|
}
|
|
|
|
func TestCrossPlatformCoverageGeminiRemainingAttachmentEdges(t *testing.T) {
|
|
origPollInterval := geminiFilePollInterval
|
|
t.Cleanup(func() { geminiFilePollInterval = origPollInterval })
|
|
f := &geminiAPIForwarder{model: "model", apiKey: "key", baseURL: "https://example.test/v1beta", timeout: 1, httpClient: http.DefaultClient}
|
|
if _, err := f.forwardWithAttachments(context.Background(), "", "text", []connectMediaAttachment{{LocalPath: filepath.Join(t.TempDir(), "missing")}}); err == nil {
|
|
t.Fatal("missing attachment forward succeeded")
|
|
}
|
|
|
|
large := filepath.Join(t.TempDir(), "large.bin")
|
|
file, err := os.Create(large)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := file.Truncate(geminiInlineRawLimit + 1); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := file.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
invalid := *f
|
|
invalid.baseURL = "%"
|
|
if _, err := invalid.partsWithAttachments(context.Background(), "text", []connectMediaAttachment{{LocalPath: large}}); err == nil {
|
|
t.Fatal("large attachment upload failure succeeded")
|
|
}
|
|
if endpoint, err := (&geminiAPIForwarder{}).filesEndpoint(false); err != nil || endpoint == "" {
|
|
t.Fatalf("default files endpoint = %q, %v", endpoint, err)
|
|
}
|
|
invalidName := *f
|
|
invalidName.httpClient = &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
|
return nil, errors.New("request should not run")
|
|
})}
|
|
geminiFilePollInterval = 0
|
|
if _, err := invalidName.waitForUploadedFile(context.Background(), geminiUploadedFile{Name: "%", State: "PROCESSING"}); err == nil {
|
|
t.Fatal("invalid poll file name succeeded")
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageCallbackMediaCrossSourceDeduplication(t *testing.T) {
|
|
content := map[string]any{
|
|
"downloadCode": "same",
|
|
"fileName": "top.txt",
|
|
"richText": []any{
|
|
map[string]any{"type": "file", "downloadCode": "same", "fileName": "nested.txt"},
|
|
},
|
|
}
|
|
_, files, _ := callbackInboundMedia("file", content)
|
|
if len(files) != 1 {
|
|
t.Fatalf("cross-source duplicate files = %#v", files)
|
|
}
|
|
pictures, files, _ := callbackInboundMedia("picture", map[string]any{"pictureDownloadCode": "picture", "downloadCode": "picture"})
|
|
if len(pictures) != 1 || len(files) != 0 {
|
|
t.Fatalf("picture/file duplicate = %v, %#v", pictures, files)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageForwarderAttachmentDispatchRemainingEdges(t *testing.T) {
|
|
streaming := &coverageStreamingAttachmentForwarder{}
|
|
if reply, err := forwardConnectTurn(context.Background(), streaming, "conv", "prompt", nil, func(string) {}); err != nil || reply != "streamed" || !streaming.called {
|
|
t.Fatalf("streaming attachment dispatch = %q, %v, called=%v", reply, err, streaming.called)
|
|
}
|
|
|
|
bin := writeShellExecutable(t, t.TempDir(), "agent", "printf 'ok\\n'\n")
|
|
for _, fwd := range []*execForwarder{
|
|
{name: "custom", argv: []string{bin}},
|
|
{name: "claudecode", argv: []string{bin}},
|
|
{name: "workbuddy", argv: []string{bin}},
|
|
} {
|
|
attachments := []connectMediaAttachment(nil)
|
|
if fwd.name == "workbuddy" {
|
|
path := filepath.Join(t.TempDir(), "file.txt")
|
|
attachments = []connectMediaAttachment{{LocalPath: ""}, {LocalPath: path}, {LocalPath: path}}
|
|
}
|
|
if reply, err := fwd.forwardWithAttachments(context.Background(), "conv", "prompt", attachments); err != nil || reply != "ok" {
|
|
t.Fatalf("exec %s = %q, %v", fwd.name, reply, err)
|
|
}
|
|
}
|
|
|
|
plainPrompt, plainAttachments := prepareConnectForwarderAttachments(&basicConnectorForwarder{}, "plain", []connectMediaAttachment{{LocalPath: "x"}})
|
|
if plainPrompt != "plain" || len(plainAttachments) != 1 {
|
|
t.Fatalf("plain prepare = %q, %#v", plainPrompt, plainAttachments)
|
|
}
|
|
origStoryboard := generateConnectVideoStoryboard
|
|
t.Cleanup(func() { generateConnectVideoStoryboard = origStoryboard })
|
|
generateConnectVideoStoryboard = func(context.Context, string) (string, error) { return "storyboard.jpg", nil }
|
|
prompt, attachments := prepareConnectForwarderAttachments(&opencodeForwarder{}, "video.mp4", []connectMediaAttachment{{LocalPath: "video.mp4", FileName: "video.mp4", MediaType: "video"}})
|
|
if !strings.Contains(prompt, "storyboard.jpg") || len(attachments) != 1 {
|
|
t.Fatalf("opencode prepare = %q, %#v", prompt, attachments)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageQoderAttachmentForwardingRemainingEdges(t *testing.T) {
|
|
cancelled, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
fwd := &qoderStreamForwarder{name: "qoder", bin: "missing-qoder", timeout: 1}
|
|
if _, err := fwd.forwardWithAttachments(cancelled, "conv", "text", nil); err == nil {
|
|
t.Fatal("cancelled attachment-free qoder forward succeeded")
|
|
}
|
|
|
|
success := writeShellExecutable(t, t.TempDir(), "qoder-ok", "printf 'answer\\n'\n")
|
|
sessions := newConvSessions("")
|
|
fwd = &qoderStreamForwarder{name: "qoderwork", bin: success, timeout: 30 * time.Second, yolo: true, model: "model", sessions: sessions}
|
|
if reply, err := fwd.forwardWithAttachments(context.Background(), "conv", "text", []connectMediaAttachment{{LocalPath: ""}, {LocalPath: "/tmp/file"}}); err != nil || reply == "" {
|
|
t.Fatalf("qoder success = %q, %v", reply, err)
|
|
}
|
|
|
|
backend := writeShellExecutable(t, t.TempDir(), "qoder-backend", "printf 'API Error: rejected\\n'\n")
|
|
fwd = &qoderStreamForwarder{name: "qoder", bin: backend, timeout: 30 * time.Second}
|
|
if reply, err := fwd.forwardWithAttachments(context.Background(), "conv", "text", []connectMediaAttachment{{LocalPath: "/tmp/file"}}); err != nil || reply == "" {
|
|
t.Fatalf("qoder backend error reply = %q, %v", reply, err)
|
|
}
|
|
|
|
failure := writeShellExecutable(t, t.TempDir(), "qoder-fail", "exit 1\n")
|
|
fwd = &qoderStreamForwarder{name: "qoder", bin: failure, timeout: 30 * time.Second, sessions: sessions}
|
|
if _, err := fwd.forwardWithAttachments(context.Background(), "conv", "text", []connectMediaAttachment{{LocalPath: "/tmp/file"}}); err == nil {
|
|
t.Fatal("qoder process failure succeeded")
|
|
}
|
|
|
|
empty := writeShellExecutable(t, t.TempDir(), "qoder-empty", "exit 0\n")
|
|
fwd = &qoderStreamForwarder{name: "qoder", bin: empty, timeout: 30 * time.Second}
|
|
if reply, err := fwd.forwardWithAttachments(context.Background(), "conv", "text", []connectMediaAttachment{{LocalPath: "/tmp/file"}}); err != nil || reply == "" {
|
|
t.Fatalf("qoder empty = %q, %v", reply, err)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageOpenCodeAndCodexAttachmentWrappers(t *testing.T) {
|
|
var requestBody string
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
raw, _ := io.ReadAll(r.Body)
|
|
requestBody = string(raw)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"parts":[{"type":"text","text":"ok"}]}`))
|
|
}))
|
|
defer server.Close()
|
|
file := filepath.Join(t.TempDir(), "attachment.txt")
|
|
if err := os.WriteFile(file, []byte("data"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
client := &opencodeHTTPClient{baseURL: server.URL, httpClient: server.Client()}
|
|
if reply, err := client.sendMessageWithAttachments(context.Background(), "session", "text", "", []connectMediaAttachment{{LocalPath: ""}, {LocalPath: file}}); err != nil || reply != "ok" {
|
|
t.Fatalf("opencode attachments = %q, %v", reply, err)
|
|
}
|
|
if !strings.Contains(requestBody, filepath.Base(file)) {
|
|
t.Fatalf("opencode request = %s", requestBody)
|
|
}
|
|
|
|
codex := &codexAppServerForwarder{bin: filepath.Join(t.TempDir(), "missing"), timeout: 1}
|
|
if _, err := codex.forwardWithAttachments(context.Background(), "conv", "text", nil); err == nil {
|
|
t.Fatal("codex wrapper unexpectedly succeeded")
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageRunStreamConnectorChatRecordLookupBranches(t *testing.T) {
|
|
origRecover := recoverChatRecordUnknownsForConnect
|
|
t.Cleanup(func() { recoverChatRecordUnknownsForConnect = origRecover })
|
|
recoverChatRecordUnknownsForConnect = func(context.Context, chatRecordLookup, chatRecordToolCall) (chatRecordEnrichment, error) {
|
|
return chatRecordEnrichment{}, errors.New("lookup failed")
|
|
}
|
|
message := func(id string) *chatbot.BotCallbackDataModel {
|
|
m := connectorMessage(id, "")
|
|
m.MsgId = id
|
|
m.Msgtype = "chatRecord"
|
|
m.Content = map[string]any{"chatRecord": []any{map[string]any{"msgType": "unknownMsgType"}}}
|
|
return m
|
|
}
|
|
fwd := &basicConnectorForwarder{reply: "done"}
|
|
runConnectorScenario(t, []*chatbot.BotCallbackDataModel{message("outer"), message("")}, fwd, nil, nil, nil, 2)
|
|
if len(fwd.prompts) != 2 {
|
|
t.Fatalf("chat record prompts = %#v", fwd.prompts)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageRemainingCommandExecutionBranches(t *testing.T) {
|
|
t.Run("aitable view with timeout", func(t *testing.T) {
|
|
caller, err := runAitableExportCommand(t,
|
|
"--base-id", "base", "--scope", "view", "--table-id", "table", "--view-id", "view",
|
|
"--export-format", "excel", "--timeout-ms", "250",
|
|
)
|
|
if err != nil || len(caller.calls) != 1 || caller.calls[0].args["timeoutMs"] != 250 {
|
|
t.Fatalf("aitable export = %#v, %v", caller.calls, err)
|
|
}
|
|
})
|
|
|
|
t.Run("doc version confirmed revert", func(t *testing.T) {
|
|
testseam.Protect(t, &deps)
|
|
caller := &scriptedToolCaller{steps: []scriptedToolStep{{text: `{"versions":[{"version":3}]}`}, {text: `{}`}}}
|
|
if err := runDocCoverageCommand(t, caller, "--yes", "version", "revert", "--node=node", "--version=3"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if caller.index != 2 {
|
|
t.Fatalf("doc caller index = %d", caller.index)
|
|
}
|
|
})
|
|
|
|
t.Run("mail invalid parameter suggestion", func(t *testing.T) {
|
|
testseam.Protect(t, &deps)
|
|
caller := &coverageErrorCaller{err: &CLIError{Code: CodeMCPToolError, Message: "Invalid parameter"}}
|
|
InitDeps(caller)
|
|
cmd := newMailCommand()
|
|
cmd.SetArgs([]string{"template", "update", "--email", "user@example.com", "--id", "id", "--subject", "updated"})
|
|
err := cmd.Execute()
|
|
var cliErr *CLIError
|
|
if !errors.As(err, &cliErr) || !strings.Contains(cliErr.Suggestion, "草稿") {
|
|
t.Fatalf("mail update error = %#v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("sheet style dry run", func(t *testing.T) {
|
|
testseam.Protect(t, &deps)
|
|
batchPath := filepath.Join(t.TempDir(), "styles.json")
|
|
if err := os.WriteFile(batchPath, []byte(`[{"sheetId":"Sheet1","range":"A1:B2","fontWeight":"bold"}]`), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
InitDeps(&coverageErrorCaller{err: errors.New("dry run failed"), dryRun: true})
|
|
cmd := newRangeBatchSetStyleCmd()
|
|
cmd.SetArgs([]string{"--node", "node", "--batch", batchPath})
|
|
if err := cmd.Execute(); err != nil {
|
|
t.Fatalf("sheet style error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("business error classification", func(t *testing.T) {
|
|
testseam.Protect(t, &deps)
|
|
InitDeps(&coverageErrorCaller{result: &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: `{"success":false,"message":"bad"}`}}}})
|
|
if err := callMCPTool("business", nil); err == nil {
|
|
t.Fatal("business error was not classified")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCrossPlatformCoverageAttachSheetConfirmationGuardMissingPath(t *testing.T) {
|
|
defer func() {
|
|
if recover() == nil {
|
|
t.Fatal("missing Sheet path did not panic")
|
|
}
|
|
}()
|
|
attachSheetConfirmationGuard(&cobra.Command{Use: "sheet"}, "missing leaf", "delete", "target")
|
|
}
|
|
|
|
func TestCrossPlatformCoverageDentryDownloadRejectsDeclaredOversize(t *testing.T) {
|
|
orig := connectMediaCallRaw
|
|
t.Cleanup(func() { connectMediaCallRaw = orig })
|
|
connectMediaCallRaw = func(*aiCardClient, context.Context, string, string, map[string]any) (string, error) {
|
|
return `{"resourceUrl":"https://download.test/file"}`, nil
|
|
}
|
|
client := &aiCardClient{httpClient: &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
ContentLength: mediaMaxDownloadBytes + 1,
|
|
Body: io.NopCloser(strings.NewReader("")),
|
|
Header: make(http.Header),
|
|
}, nil
|
|
})}}
|
|
if _, err := client.downloadDentryFile(context.Background(), 1, 2, "union", "file"); err == nil || !strings.Contains(err.Error(), "过大") {
|
|
t.Fatalf("oversize dentry error = %v", err)
|
|
}
|
|
}
|