518 lines
27 KiB
Go
518 lines
27 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0
|
|
|
|
package minutes
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers"
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/localio"
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
type minutesFailWriter struct{}
|
|
|
|
func (minutesFailWriter) Write([]byte) (int, error) { return 0, errors.New("fixture output failure") }
|
|
|
|
func runMinutesAlignmentCLIWithWriter(t *testing.T, caller *minutesE2ECaller, writer io.Writer, args ...string) error {
|
|
t.Helper()
|
|
helpers.InitDepsForTest(t, caller)
|
|
root := &cobra.Command{Use: "dws", SilenceErrors: true, SilenceUsage: true}
|
|
root.PersistentFlags().Bool("yes", false, "")
|
|
root.PersistentFlags().Bool("dry-run", false, "")
|
|
root.PersistentFlags().String("format", "json", "")
|
|
root.AddCommand(shortcut.Commands()...)
|
|
root.SetOut(writer)
|
|
root.SetErr(&bytes.Buffer{})
|
|
root.SetArgs(args)
|
|
return root.Execute()
|
|
}
|
|
|
|
func TestCrossPlatformCoverageMinutesAlignmentValidationAndHelpers(t *testing.T) {
|
|
caller := &minutesE2ECaller{}
|
|
manyIDs := make([]string, 51)
|
|
for index := range manyIDs {
|
|
manyIDs[index] = fmt.Sprintf("u%d", index)
|
|
}
|
|
invalid := [][]string{
|
|
{"minutes", "+search", "--query", "x", "--limit", "0"},
|
|
{"minutes", "+search", "--query", "x", "--start", "bad"},
|
|
{"minutes", "+search", "--query", "x", "--end", "bad"},
|
|
{"minutes", "+search", "--query", "x", "--start", "2026-08-02T00:00:00Z", "--end", "2026-08-01T00:00:00Z"},
|
|
{"minutes", "+download", "--id", "u1", "--ids", "u2"},
|
|
{"minutes", "+download", "--ids", strings.Join(manyIDs, ",")},
|
|
{"minutes", "+download", "--ids", "u1,u2", "--output", "one"},
|
|
{"minutes", "+download", "--id", "u1", "--output", "../escape"},
|
|
{"minutes", "+upload", "--file", "missing", "--complete-timeout", "0"},
|
|
{"minutes", "+speaker-replace", "--id", "u1", "--from", "same", "--to", " same "},
|
|
{"minutes", "+speaker-replace", "--id", "u1", "--from", "a", "--to", "b", "--page-limit", "0"},
|
|
}
|
|
for _, args := range invalid {
|
|
if payload, output, err := runMinutesAlignmentCLI(t, caller, args...); err == nil || payload != nil || output != "" {
|
|
t.Fatalf("invalid argv accepted: %v payload=%#v output=%q err=%v", args, payload, output, err)
|
|
}
|
|
}
|
|
|
|
if start, end, err := minutesTimeRange("2026-08-01T00:00:00Z", "2026-08-02T00:00:00Z"); err != nil || start == 0 || end <= start {
|
|
t.Fatalf("valid range = %d %d %v", start, end, err)
|
|
}
|
|
if got := mediaExtension("https://example.invalid/a.MP3?token=x"); got != ".mp3" {
|
|
t.Fatalf("extension = %q", got)
|
|
}
|
|
if got := mediaExtension("://bad"); got != "" {
|
|
t.Fatalf("bad extension = %q", got)
|
|
}
|
|
if got := mediaExtension("https://example.invalid/a.12345678901"); got != "" {
|
|
t.Fatalf("long extension = %q", got)
|
|
}
|
|
counts := speakerCounts([]map[string]any{{
|
|
"speakerNick": " A ", "nested": []any{map[string]any{"nickName": "A", "subSpeakerNickname": "B"}},
|
|
}})
|
|
if counts["A"] != 1 || counts["B"] != 1 {
|
|
t.Fatalf("speaker counts = %#v", counts)
|
|
}
|
|
|
|
decl := corecmd.ContractDecl{Parameters: []contract.ParamDecl{
|
|
{Name: "a", Description: "parameter"},
|
|
{Name: "a", Description: "already evidence"},
|
|
{Name: "other", Description: "other"},
|
|
}}
|
|
value := shortcut.Shortcut{Description: "desc", Intent: "intent", Contract: decl,
|
|
Flags: []shortcut.Flag{{Name: "a", Desc: "flag"}},
|
|
Constraints: []shortcut.Constraint{
|
|
{Kind: shortcut.ConstraintAtLeastOne, Flags: []string{"a"}},
|
|
{Kind: shortcut.ConstraintCustom, Flags: []string{"a"}},
|
|
},
|
|
}
|
|
finalized := finalizeMinutesShortcuts(value)[0]
|
|
if finalized.Contract.Selection.AgentSummary != "desc" || strings.Contains(finalized.Flags[0].Desc, "约束") {
|
|
t.Fatalf("finalized = %#v", finalized)
|
|
}
|
|
withEvidence := value
|
|
withEvidence.Constraints = []shortcut.Constraint{{Kind: shortcut.ConstraintCustom, Flags: []string{"a"}, Description: "evidence"}}
|
|
withEvidence.Flags[0].Desc = "flag evidence"
|
|
withEvidence.Contract.Parameters[1].Description = "already evidence"
|
|
finalized = finalizeMinutesShortcuts(withEvidence)[0]
|
|
if !strings.Contains(finalized.Contract.Parameters[0].Description, "约束:evidence") || finalized.Contract.Parameters[1].Description != "already evidence" || finalized.Contract.Parameters[2].Description != "other" {
|
|
t.Fatalf("parameter evidence = %#v", finalized.Contract.Parameters)
|
|
}
|
|
payload := minutesDryRunPayload("plan", "op", nil)
|
|
if payload["operation"] != "op" || payload["executed"] != false {
|
|
t.Fatalf("dry-run payload = %#v", payload)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageMinutesSearchFailureAndCursorBranchesE2E(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
caller *minutesE2ECaller
|
|
args []string
|
|
wantError bool
|
|
wantNext string
|
|
}{
|
|
{name: "call", caller: &minutesE2ECaller{failAt: map[string]int{"minutes/list_by_keyword_and_time_range": 1}}, args: []string{"minutes", "+search", "--query", "x"}, wantError: true},
|
|
{name: "parse", caller: &minutesE2ECaller{responses: map[string][]string{"minutes/list_by_keyword_and_time_range": {`{"success":true,"result":{}}`}}}, args: []string{"minutes", "+search", "--query", "x"}, wantError: true},
|
|
{name: "project", caller: &minutesE2ECaller{responses: map[string][]string{"minutes/list_by_keyword_and_time_range": {`{"success":true,"result":{"itemList":[{"title":"x"}],"hasNext":false}}`}}}, args: []string{"minutes", "+search", "--query", "x"}, wantError: true},
|
|
{name: "single page", caller: &minutesE2ECaller{responses: map[string][]string{"minutes/list_by_keyword_and_time_range": {`{"success":true,"result":{"itemList":[{"uuid":"u1","title":"x"}],"hasNext":true,"nextToken":"n2"}}`}}}, args: []string{"minutes", "+search", "--start", "2026-08-01T00:00:00Z", "--end", "2026-08-02T00:00:00Z", "--cursor", "n1"}, wantNext: "n2"},
|
|
{name: "limit", caller: &minutesE2ECaller{responses: map[string][]string{"minutes/list_by_keyword_and_time_range": {`{"success":true,"result":{"itemList":[],"hasNext":true,"nextToken":"n2"}}`}}}, args: []string{"minutes", "+search", "--query", "x", "--page-all", "--page-limit", "1"}, wantError: true},
|
|
{name: "cycle", caller: &minutesE2ECaller{responses: map[string][]string{"minutes/list_by_keyword_and_time_range": {`{"success":true,"result":{"itemList":[],"hasNext":true,"nextToken":"n1"}}`}}}, args: []string{"minutes", "+search", "--query", "x", "--page-all", "--cursor", "n1"}, wantError: true},
|
|
}
|
|
for _, test := range cases {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
payload, _, err := runMinutesAlignmentCLI(t, test.caller, test.args...)
|
|
if (err != nil) != test.wantError {
|
|
t.Fatalf("payload=%#v err=%v", payload, err)
|
|
}
|
|
if test.wantNext != "" && payload["nextToken"] != test.wantNext {
|
|
t.Fatalf("payload=%#v", payload)
|
|
}
|
|
})
|
|
}
|
|
if err := runMinutesAlignmentCLIWithWriter(t, &minutesE2ECaller{responses: map[string][]string{
|
|
"minutes/list_by_keyword_and_time_range": {`{"success":true,"result":{"itemList":[],"hasNext":false}}`},
|
|
}}, minutesFailWriter{}, "minutes", "+search", "--query", "x"); err == nil {
|
|
t.Fatal("output failure accepted")
|
|
}
|
|
// Execute is defensive even when invoked without the Shortcut validator.
|
|
cmd := &cobra.Command{Use: "search"}
|
|
cmd.Flags().String("start", "bad", "")
|
|
cmd.Flags().String("end", "", "")
|
|
cmd.Flags().String("scope", "all", "")
|
|
cmd.Flags().String("cursor", "", "")
|
|
cmd.Flags().String("query", "x", "")
|
|
cmd.Flags().Int("limit", 20, "")
|
|
cmd.Flags().Int("page-limit", 100, "")
|
|
cmd.Flags().Bool("page-all", false, "")
|
|
if err := executeMinutesSearch(shortcut.RuntimeContextForTest(cmd, Search)); err == nil {
|
|
t.Fatal("direct invalid time accepted")
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageMinutesDownloadBranchesE2E(t *testing.T) {
|
|
testseam.Swap(t, &minutesDownload, func(_ context.Context, rawURL string, options localio.DownloadOptions) (localio.DownloadResult, error) {
|
|
if strings.Contains(rawURL, "fail") {
|
|
return localio.DownloadResult{}, errors.New("fixture download failure")
|
|
}
|
|
return localio.DownloadResult{RelativePath: filepath.ToSlash(filepath.Join(options.Output, options.PreferredName)), SizeBytes: 12}, nil
|
|
})
|
|
caller := &minutesE2ECaller{
|
|
responses: map[string][]string{"minutes/query_minutes_audio_url": {
|
|
`{"success":true,"result":{"downloadUrl":"https://example.invalid/u1.mp3"}}`,
|
|
`{"success":true,"result":{"downloadUrl":"https://example.invalid/fail.mp4"}}`,
|
|
}},
|
|
failAt: map[string]int{"minutes/query_minutes_audio_url": 3},
|
|
}
|
|
payload, output, err := runMinutesAlignmentCLI(t, caller, "minutes", "+download", "--ids", "u1,u2,u3", "--output-dir", "media")
|
|
if err == nil || output == "" || payload["succeeded"] != float64(1) || payload["failed"] != float64(2) {
|
|
t.Fatalf("download payload=%#v err=%v", payload, err)
|
|
}
|
|
urlOnly := &minutesE2ECaller{responses: map[string][]string{"minutes/query_minutes_audio_url": {`{"success":true,"result":{"downloadUrl":"https://example.invalid/u1.mp3"}}`}}}
|
|
if payload, _, err = runMinutesAlignmentCLI(t, urlOnly, "minutes", "+download", "--id", "u1", "--url-only"); err != nil || payload["succeeded"] != float64(1) {
|
|
t.Fatalf("url-only payload=%#v err=%v", payload, err)
|
|
}
|
|
if err := runMinutesAlignmentCLIWithWriter(t, urlOnly, minutesFailWriter{}, "minutes", "+download", "--id", "u1", "--url-only"); err == nil {
|
|
t.Fatal("download output failure accepted")
|
|
}
|
|
single := &minutesE2ECaller{responses: map[string][]string{"minutes/query_minutes_audio_url": {`{"success":true,"result":{"downloadUrl":"https://example.invalid/u1.mp3"}}`}}}
|
|
if payload, _, err = runMinutesAlignmentCLI(t, single, "minutes", "+download", "--id", "u1", "--output", "single.mp3"); err != nil || payload["succeeded"] != float64(1) {
|
|
t.Fatalf("single payload=%#v err=%v", payload, err)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageMinutesUploadFailureAndSuccessBranchesE2E(t *testing.T) {
|
|
file := filepath.Join(t.TempDir(), "source.wav")
|
|
if err := os.WriteFile(file, []byte("audio"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if payload, output, err := runMinutesAlignmentCLI(t, &minutesE2ECaller{}, "minutes", "+upload", "--file", t.TempDir(), "--yes"); err == nil || payload != nil || output != "" {
|
|
t.Fatalf("directory upload accepted: %#v %q %v", payload, output, err)
|
|
}
|
|
|
|
createFail := &minutesE2ECaller{failAt: map[string]int{"minutes/create_upload_session": 1}}
|
|
if _, _, err := runMinutesAlignmentCLI(t, createFail, "minutes", "+upload", "--file", file, "--yes"); err == nil {
|
|
t.Fatal("create failure accepted")
|
|
}
|
|
badSession := &minutesE2ECaller{responses: map[string][]string{"minutes/create_upload_session": {`{"success":true,"result":{}}`}}}
|
|
if _, _, err := runMinutesAlignmentCLI(t, badSession, "minutes", "+upload", "--file", file, "--yes"); err == nil {
|
|
t.Fatal("bad session accepted")
|
|
}
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
cancelRaw string
|
|
cancelFailAt int
|
|
}{
|
|
{name: "cancel success", cancelRaw: `{"success":true,"result":{}}`},
|
|
{name: "cancel call failure", cancelFailAt: 1},
|
|
{name: "cancel acknowledgement failure", cancelRaw: `{"result":{}}`},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
testseam.Swap(t, &minutesPutFile, func(context.Context, string, string, int64) (localio.UploadResult, error) {
|
|
return localio.UploadResult{}, errors.New("put failed")
|
|
})
|
|
caller := &minutesE2ECaller{
|
|
responses: map[string][]string{
|
|
"minutes/create_upload_session": {`{"success":true,"result":{"sessionId":"s1","presignedUrl":"https://example.invalid/u"}}`},
|
|
"minutes/cancel_upload_session": {test.cancelRaw},
|
|
},
|
|
failAt: map[string]int{"minutes/cancel_upload_session": test.cancelFailAt},
|
|
}
|
|
if _, _, err := runMinutesAlignmentCLI(t, caller, "minutes", "+upload", "--file", file, "--yes"); err == nil {
|
|
t.Fatal("put failure accepted")
|
|
}
|
|
})
|
|
}
|
|
|
|
testseam.Swap(t, &minutesPutFile, func(context.Context, string, string, int64) (localio.UploadResult, error) {
|
|
return localio.UploadResult{SizeBytes: 5, Attempts: 2}, nil
|
|
})
|
|
base := map[string][]string{
|
|
"minutes/create_upload_session": {`{"success":true,"result":{"sessionId":"s1","presignedUrl":"https://example.invalid/u"}}`},
|
|
"minutes/complete_upload_session": {`{"success":true,"result":{"taskUuid":"u1"}}`},
|
|
"minutes/get_minutes_basic_info": {`{"success":true,"result":{"taskUuid":"u1","title":"ok"}}`},
|
|
}
|
|
success := &minutesE2ECaller{responses: base}
|
|
payload, _, err := runMinutesAlignmentCLI(t, success, "minutes", "+upload", "--file", file, "--title", "T", "--template-id", "tpl", "--input-language", "zh", "--enable-message-card", "--yes")
|
|
if err != nil || payload["complete"] != true || payload["verified"] != true {
|
|
t.Fatalf("upload payload=%#v err=%v", payload, err)
|
|
}
|
|
|
|
cases := []struct {
|
|
name string
|
|
responses map[string][]string
|
|
failAt map[string]int
|
|
}{
|
|
{name: "invalid completion", responses: map[string][]string{
|
|
"minutes/create_upload_session": {`{"success":true,"result":{"sessionId":"s1","presignedUrl":"https://example.invalid/u"}}`},
|
|
"minutes/complete_upload_session": {`{"success":true,"result":{}}`},
|
|
}},
|
|
{name: "verify call", responses: base, failAt: map[string]int{"minutes/get_minutes_basic_info": 1}},
|
|
{name: "verify parse", responses: map[string][]string{
|
|
"minutes/create_upload_session": {`{"success":true,"result":{"sessionId":"s1","presignedUrl":"https://example.invalid/u"}}`},
|
|
"minutes/complete_upload_session": {`{"success":true,"result":{"taskUuid":"u1"}}`},
|
|
"minutes/get_minutes_basic_info": {`{"success":true,"result":{}}`},
|
|
}},
|
|
}
|
|
for _, test := range cases {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
caller := &minutesE2ECaller{responses: test.responses, failAt: test.failAt}
|
|
if _, _, err := runMinutesAlignmentCLI(t, caller, "minutes", "+upload", "--file", file, "--yes"); err == nil {
|
|
t.Fatal("upload failure accepted")
|
|
}
|
|
})
|
|
}
|
|
if err := runMinutesAlignmentCLIWithWriter(t, &minutesE2ECaller{}, minutesFailWriter{}, "minutes", "+upload", "--file", file, "--dry-run"); err == nil {
|
|
t.Fatal("upload output failure accepted")
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageMinutesUploadErrorConstructors(t *testing.T) {
|
|
if err := minutesUploadFailure("put", "s", false, nil, nil); err == nil {
|
|
t.Fatal("nil-cause failure missing")
|
|
}
|
|
if err := minutesUploadCompletionUnknown("s", 1, nil); err == nil {
|
|
t.Fatal("nil-cause unknown completion missing")
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageMinutesUpdatePermissionSummarySpeakerFailuresE2E(t *testing.T) {
|
|
// Dry-run branches must not access MCP and must still propagate output errors.
|
|
dryCases := [][]string{
|
|
{"minutes", "+update", "--id", "u1", "--title", "new", "--dry-run"},
|
|
{"minutes", "+summary", "--id", "u1", "--content", "new", "--dry-run"},
|
|
{"minutes", "+speaker-replace", "--id", "u1", "--from", "a", "--to", "b", "--dry-run"},
|
|
}
|
|
for _, args := range dryCases {
|
|
if err := runMinutesAlignmentCLIWithWriter(t, &minutesE2ECaller{}, minutesFailWriter{}, args...); err == nil {
|
|
t.Fatalf("output failure accepted: %v", args)
|
|
}
|
|
}
|
|
|
|
updateCases := []struct {
|
|
name string
|
|
responses map[string][]string
|
|
failAt map[string]int
|
|
wantError bool
|
|
}{
|
|
{name: "before call", failAt: map[string]int{"minutes/get_minutes_basic_info": 1}, wantError: true},
|
|
{name: "before parse", responses: map[string][]string{"minutes/get_minutes_basic_info": {`{"success":true,"result":{}}`}}, wantError: true},
|
|
{name: "unchanged", responses: map[string][]string{"minutes/get_minutes_basic_info": {`{"success":true,"result":{"taskUuid":"u1","title":"new"}}`}}},
|
|
{name: "write", responses: map[string][]string{"minutes/get_minutes_basic_info": {`{"success":true,"result":{"taskUuid":"u1","title":"old"}}`}}, failAt: map[string]int{"minutes/update_minutes_title": 1}, wantError: true},
|
|
{name: "after call", responses: map[string][]string{"minutes/get_minutes_basic_info": {`{"success":true,"result":{"taskUuid":"u1","title":"old"}}`}}, failAt: map[string]int{"minutes/get_minutes_basic_info": 2}, wantError: true},
|
|
{name: "after parse", responses: map[string][]string{"minutes/get_minutes_basic_info": {`{"success":true,"result":{"taskUuid":"u1","title":"old"}}`, `{"success":true,"result":{}}`}, "minutes/update_minutes_title": {`{"success":true,"result":{}}`}}, wantError: true},
|
|
}
|
|
for _, test := range updateCases {
|
|
t.Run("update "+test.name, func(t *testing.T) {
|
|
payload, _, err := runMinutesAlignmentCLI(t, &minutesE2ECaller{responses: test.responses, failAt: test.failAt}, "minutes", "+update", "--id", "u1", "--title", "new", "--yes")
|
|
if (err != nil) != test.wantError {
|
|
t.Fatalf("payload=%#v err=%v", payload, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
permissionCases := []struct {
|
|
name string
|
|
responses map[string][]string
|
|
failAt map[string]int
|
|
writer io.Writer
|
|
}{
|
|
{name: "call", failAt: map[string]int{"minutes/apply_minutes_permission": 1}},
|
|
{name: "ack", responses: map[string][]string{"minutes/apply_minutes_permission": {`{"result":{}}`}}},
|
|
{name: "success output", responses: map[string][]string{"minutes/apply_minutes_permission": {`{"success":true,"result":{}}`}}, writer: minutesFailWriter{}},
|
|
}
|
|
for _, test := range permissionCases {
|
|
t.Run("permission "+test.name, func(t *testing.T) {
|
|
writer := test.writer
|
|
if writer == nil {
|
|
writer = &bytes.Buffer{}
|
|
}
|
|
if err := runMinutesAlignmentCLIWithWriter(t, &minutesE2ECaller{responses: test.responses, failAt: test.failAt}, writer, "minutes", "+apply-permission", "--id", "u1", "--permission", "view", "--yes"); err == nil {
|
|
t.Fatal("permission failure accepted")
|
|
}
|
|
})
|
|
}
|
|
permissionSuccess := &minutesE2ECaller{responses: map[string][]string{"minutes/apply_minutes_permission": {`{"success":true,"result":{}}`}}}
|
|
if payload, _, err := runMinutesAlignmentCLI(t, permissionSuccess, "minutes", "+apply-permission", "--id", "u1", "--permission", "view", "--yes"); err != nil || payload["requested"] != true {
|
|
t.Fatalf("permission payload=%#v err=%v", payload, err)
|
|
}
|
|
|
|
if _, _, err := runMinutesAlignmentCLI(t, &minutesE2ECaller{}, "minutes", "+summary", "--id", "u1", "--content", "@missing", "--yes"); err == nil {
|
|
t.Fatal("missing input accepted")
|
|
}
|
|
if _, _, err := runMinutesAlignmentCLI(t, &minutesE2ECaller{}, "minutes", "+summary", "--id", "u1", "--content", " ", "--yes"); err == nil {
|
|
t.Fatal("empty summary accepted")
|
|
}
|
|
cmd := &cobra.Command{Use: "summary"}
|
|
cmd.Flags().String("id", "u1", "")
|
|
cmd.Flags().String("content", " ", "")
|
|
if err := executeMinutesSummary(shortcut.RuntimeContextForTest(cmd, Summary)); err == nil {
|
|
t.Fatal("direct empty summary accepted")
|
|
}
|
|
summaryCases := []struct {
|
|
name string
|
|
responses map[string][]string
|
|
failAt map[string]int
|
|
wantError bool
|
|
}{
|
|
{name: "before call", failAt: map[string]int{"minutes/get_minutes_ai_summary": 1}, wantError: true},
|
|
{name: "before parse", responses: map[string][]string{"minutes/get_minutes_ai_summary": {`{"success":true,"result":{}}`}}, wantError: true},
|
|
{name: "unchanged", responses: map[string][]string{"minutes/get_minutes_ai_summary": {`{"success":true,"result":{"fullSummary":"new"}}`}}},
|
|
{name: "write", responses: map[string][]string{"minutes/get_minutes_ai_summary": {`{"success":true,"result":{"fullSummary":"old"}}`}}, failAt: map[string]int{"minutes/update_minutes_summary": 1}, wantError: true},
|
|
{name: "readback call", responses: map[string][]string{"minutes/get_minutes_ai_summary": {`{"success":true,"result":{"fullSummary":"old"}}`}}, failAt: map[string]int{"minutes/get_minutes_ai_summary": 2}, wantError: true},
|
|
{name: "readback parse", responses: map[string][]string{"minutes/get_minutes_ai_summary": {`{"success":true,"result":{"fullSummary":"old"}}`, `{"success":true,"result":{}}`}, "minutes/update_minutes_summary": {`{"success":true,"result":{}}`}}, wantError: true},
|
|
}
|
|
for _, test := range summaryCases {
|
|
t.Run("summary "+test.name, func(t *testing.T) {
|
|
payload, _, err := runMinutesAlignmentCLI(t, &minutesE2ECaller{responses: test.responses, failAt: test.failAt}, "minutes", "+summary", "--id", "u1", "--content", "new", "--yes")
|
|
if (err != nil) != test.wantError {
|
|
t.Fatalf("payload=%#v err=%v", payload, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
speakerCases := []struct {
|
|
name string
|
|
responses map[string][]string
|
|
failAt map[string]int
|
|
}{
|
|
{name: "before", failAt: map[string]int{"minutes/get_minutes_transcription": 1}},
|
|
{name: "missing", responses: map[string][]string{"minutes/get_minutes_transcription": {`{"success":true,"result":{"paragraphList":[{"speakerNick":"x"}],"hasNext":false}}`}}},
|
|
{name: "write", responses: map[string][]string{"minutes/get_minutes_transcription": {`{"success":true,"result":{"paragraphList":[{"speakerNick":"a"}],"hasNext":false}}`}}, failAt: map[string]int{"minutes/replace_speaker": 1}},
|
|
{name: "after", responses: map[string][]string{"minutes/get_minutes_transcription": {`{"success":true,"result":{"paragraphList":[{"speakerNick":"a"}],"hasNext":false}}`}}, failAt: map[string]int{"minutes/get_minutes_transcription": 2}},
|
|
{name: "verify", responses: map[string][]string{"minutes/get_minutes_transcription": {`{"success":true,"result":{"paragraphList":[{"speakerNick":"a"}],"hasNext":false}}`, `{"success":true,"result":{"paragraphList":[{"speakerNick":"a"}],"hasNext":false}}`}, "minutes/replace_speaker": {`{"success":true,"result":{}}`}}},
|
|
}
|
|
for _, test := range speakerCases {
|
|
t.Run("speaker "+test.name, func(t *testing.T) {
|
|
if _, _, err := runMinutesAlignmentCLI(t, &minutesE2ECaller{responses: test.responses, failAt: test.failAt}, "minutes", "+speaker-replace", "--id", "u1", "--from", "a", "--to", "b", "--target-uid", "uid", "--yes"); err == nil {
|
|
t.Fatal("speaker failure accepted")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageMinutesCompleteUploadRetryAndContext(t *testing.T) {
|
|
// Use the real runtime/caller path so retry classification is tested, while
|
|
// zero intervals keep the fault-injection cases deterministic and fast.
|
|
for _, test := range []struct {
|
|
name string
|
|
responses []string
|
|
failAt int
|
|
failError error
|
|
cancel bool
|
|
timeout time.Duration
|
|
interval time.Duration
|
|
wantCalls int
|
|
wantError bool
|
|
}{
|
|
{name: "success", responses: []string{`{"success":true,"result":{"taskUuid":"u1"}}`}, timeout: time.Second, wantCalls: 1},
|
|
{name: "retry success", responses: []string{`{"success":true,"result":{"taskUuid":"u1"}}`}, failAt: 1, failError: errors.New("still uploading"), timeout: time.Second, wantCalls: 2},
|
|
{name: "nonretryable", failAt: 1, failError: errors.New("denied"), timeout: time.Second, wantCalls: 1, wantError: true},
|
|
{name: "timeout", failAt: 1, failError: errors.New("wait and retry"), wantCalls: 1, wantError: true},
|
|
{name: "context", failAt: 1, failError: errors.New("正在上传"), cancel: true, timeout: 2 * time.Hour, interval: time.Hour, wantCalls: 1, wantError: true},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
key := "minutes/complete_upload_session"
|
|
caller := &minutesE2ECaller{
|
|
responses: map[string][]string{key: test.responses},
|
|
failAt: map[string]int{key: test.failAt},
|
|
failErrors: map[string]error{key: test.failError},
|
|
}
|
|
helpers.InitDepsForTest(t, caller)
|
|
ctx := context.Background()
|
|
if test.cancel {
|
|
var cancel context.CancelFunc
|
|
ctx, cancel = context.WithCancel(ctx)
|
|
cancel()
|
|
}
|
|
cmd := &cobra.Command{Use: "test"}
|
|
cmd.SetContext(ctx)
|
|
rt := shortcut.RuntimeContextForTest(cmd, Upload)
|
|
_, attempts, err := completeMinutesUpload(rt, "s1", test.timeout, test.interval)
|
|
if (err != nil) != test.wantError || attempts != test.wantCalls || caller.counts[key] != test.wantCalls {
|
|
t.Fatalf("attempts=%d calls=%d err=%v", attempts, caller.counts[key], err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageMinutesLegacyListsAndRecordCommandsE2E(t *testing.T) {
|
|
listSuccess := &minutesE2ECaller{responses: map[string][]string{"minutes/list_by_keyword_and_time_range": {`{"success":true,"result":{"itemList":[{"uuid":"u1","title":"weekly"}],"hasNext":false}}`}}}
|
|
for _, command := range []string{"+list-mine", "+list-shared", "+list-all"} {
|
|
payload, _, err := runMinutesAlignmentCLI(t, listSuccess, "minutes", command, "--query", "weekly", "--limit", "5", "--cursor", "n1")
|
|
if err != nil || payload["count"] != float64(1) {
|
|
t.Fatalf("%s payload=%#v err=%v", command, payload, err)
|
|
}
|
|
}
|
|
if calls := listSuccess.arguments["minutes/list_by_keyword_and_time_range"]; len(calls) != 3 || calls[0]["maxResults"] != 5 || calls[0]["keyword"] != "weekly" || calls[0]["nextToken"] != "n1" {
|
|
t.Fatalf("list args=%#v", calls)
|
|
}
|
|
listCall := &minutesE2ECaller{failAt: map[string]int{"minutes/list_by_keyword_and_time_range": 1}}
|
|
if _, _, err := runMinutesAlignmentCLI(t, listCall, "minutes", "+list-all"); err == nil {
|
|
t.Fatal("list call failure accepted")
|
|
}
|
|
listParse := &minutesE2ECaller{responses: map[string][]string{"minutes/list_by_keyword_and_time_range": {`{"success":true,"result":{}}`}}}
|
|
if _, _, err := runMinutesAlignmentCLI(t, listParse, "minutes", "+list-all"); err == nil {
|
|
t.Fatal("list parse failure accepted")
|
|
}
|
|
listProject := &minutesE2ECaller{responses: map[string][]string{"minutes/list_by_keyword_and_time_range": {`{"success":true,"result":{"itemList":[{"title":"missing id"}]}}`}}}
|
|
if _, _, err := runMinutesAlignmentCLI(t, listProject, "minutes", "+list-all"); err == nil {
|
|
t.Fatal("list projection failure accepted")
|
|
}
|
|
if err := runMinutesAlignmentCLIWithWriter(t, listSuccess, minutesFailWriter{}, "minutes", "+list-all"); err == nil {
|
|
t.Fatal("list output failure accepted")
|
|
}
|
|
|
|
for _, test := range []struct {
|
|
command string
|
|
cmd string
|
|
id string
|
|
}{
|
|
{command: "+record-start", cmd: "create"},
|
|
{command: "+record-pause", cmd: "pause", id: "u1"},
|
|
{command: "+record-resume", cmd: "resume", id: "u1"},
|
|
{command: "+record-stop", cmd: "end", id: "u1"},
|
|
} {
|
|
t.Run(test.command, func(t *testing.T) {
|
|
result := fmt.Sprintf(`{"success":true,"result":{"cmd":%q,"uuid":%q}}`, test.cmd, test.id)
|
|
caller := &minutesE2ECaller{responses: map[string][]string{"minutes/" + listeningNoteCmdTool: {result}}}
|
|
args := []string{"minutes", test.command, "--session-id", "session", "--yes"}
|
|
if test.id != "" {
|
|
args = append(args, "--id", test.id)
|
|
}
|
|
payload, _, err := runMinutesAlignmentCLI(t, caller, args...)
|
|
if err != nil || payload["accepted"] != true || payload["command"] != test.cmd {
|
|
t.Fatalf("payload=%#v err=%v", payload, err)
|
|
}
|
|
if caller.arguments["minutes/"+listeningNoteCmdTool][0]["sessionId"] != "session" {
|
|
t.Fatalf("args=%#v", caller.arguments)
|
|
}
|
|
})
|
|
}
|
|
recordCall := &minutesE2ECaller{failAt: map[string]int{"minutes/" + listeningNoteCmdTool: 1}}
|
|
if _, _, err := runMinutesAlignmentCLI(t, recordCall, "minutes", "+record-start", "--yes"); err == nil {
|
|
t.Fatal("record call failure accepted")
|
|
}
|
|
recordParse := &minutesE2ECaller{responses: map[string][]string{"minutes/" + listeningNoteCmdTool: {`{"success":true,"result":{"cmd":"pause","uuid":"u1"}}`}}}
|
|
if _, _, err := runMinutesAlignmentCLI(t, recordParse, "minutes", "+record-stop", "--id", "u1", "--yes"); err == nil {
|
|
t.Fatal("record result mismatch accepted")
|
|
}
|
|
recordOutput := &minutesE2ECaller{responses: map[string][]string{"minutes/" + listeningNoteCmdTool: {`{"success":true,"result":{"cmd":"end","uuid":"u1"}}`}}}
|
|
if err := runMinutesAlignmentCLIWithWriter(t, recordOutput, minutesFailWriter{}, "minutes", "+record-stop", "--id", "u1", "--yes"); err == nil {
|
|
t.Fatal("record output failure accepted")
|
|
}
|
|
}
|