668 lines
34 KiB
Go
668 lines
34 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0
|
|
|
|
package minutes
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"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"
|
|
)
|
|
|
|
func TestCrossPlatformCoverageMinutesWorkflowValidationAndDefaults(t *testing.T) {
|
|
file := filepath.Join(t.TempDir(), "source.wav")
|
|
if err := os.WriteFile(file, []byte("audio"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
invalid := [][]string{
|
|
{"minutes", "+record-wrap-up", "--id", "u1", "--wait-timeout", "0"},
|
|
{"minutes", "+upload-and-analyze", "--file", file, "--complete-timeout", "0"},
|
|
{"minutes", "+mindmap", "--id", "u1", "--timeout", "0"},
|
|
{"minutes", "+speaker-insights", "--id", "u1", "--interval", "0"},
|
|
{"minutes", "+export-pack", "--id", "u1", "--output", "pack", "--page-limit", "0"},
|
|
{"minutes", "+share", "--id", "u1", "--member-uids", strings.Repeat("m,", 51), "--permission", "view"},
|
|
}
|
|
for _, args := range invalid {
|
|
if payload, output, err := runMinutesAlignmentCLI(t, &minutesE2ECaller{}, args...); err == nil || payload != nil || output != "" {
|
|
t.Fatalf("invalid workflow accepted: %v payload=%#v output=%q err=%v", args, payload, output, err)
|
|
}
|
|
}
|
|
|
|
caller := &minutesE2ECaller{}
|
|
helpers.InitDepsForTest(t, caller)
|
|
cmd := &cobra.Command{Use: "workflow"}
|
|
cmd.Flags().StringSlice("artifacts", nil, "")
|
|
rt := shortcut.RuntimeContextForTest(cmd, RecordWrapUp)
|
|
selected := selectedWorkflowArtifacts(rt)
|
|
selected[0] = "mutated"
|
|
if minutesWorkflowArtifacts[0] == "mutated" || len(selected) != len(minutesWorkflowArtifacts) {
|
|
t.Fatalf("default artifacts alias global: %#v", selected)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageMinutesRecordWrapUpBranchesE2E(t *testing.T) {
|
|
if err := runMinutesAlignmentCLIWithWriter(t, &minutesE2ECaller{}, minutesFailWriter{}, "minutes", "+record-wrap-up", "--id", "u1", "--dry-run"); err == nil {
|
|
t.Fatal("record dry-run output failure accepted")
|
|
}
|
|
stopFailure := &minutesE2ECaller{failAt: map[string]int{"minutes/" + listeningNoteCmdTool: 1}}
|
|
if _, _, err := runMinutesAlignmentCLI(t, stopFailure, "minutes", "+record-wrap-up", "--id", "u1", "--yes"); err == nil {
|
|
t.Fatal("record stop failure accepted")
|
|
}
|
|
badStop := &minutesE2ECaller{responses: map[string][]string{"minutes/" + listeningNoteCmdTool: {`{"success":true,"result":{"cmd":"pause","uuid":"u1"}}`}}}
|
|
if _, _, err := runMinutesAlignmentCLI(t, badStop, "minutes", "+record-wrap-up", "--id", "u1", "--yes"); err == nil {
|
|
t.Fatal("record mismatched acknowledgement accepted")
|
|
}
|
|
success := &minutesE2ECaller{responses: map[string][]string{
|
|
"minutes/" + listeningNoteCmdTool: {`{"success":true,"result":{"cmd":"end","uuid":"u1"}}`},
|
|
"minutes/get_minutes_basic_info": {`{"success":true,"result":{"taskUuid":"u1","title":"done"}}`},
|
|
}}
|
|
payload, output, err := runMinutesAlignmentCLI(t, success, "minutes", "+record-wrap-up", "--id", "u1", "--artifacts", "basic", "--yes")
|
|
if err != nil || output == "" || payload["complete"] != true {
|
|
t.Fatalf("record success payload=%#v err=%v", payload, err)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageMinutesUploadAndAnalyzeBranchesE2E(t *testing.T) {
|
|
if err := runMinutesAlignmentCLIWithWriter(t, &minutesE2ECaller{}, minutesFailWriter{}, "minutes", "+upload-and-analyze", "--resume-id", "u1", "--dry-run"); err == nil {
|
|
t.Fatal("resume dry-run output failure accepted")
|
|
}
|
|
if _, _, err := runMinutesAlignmentCLI(t, &minutesE2ECaller{}, "minutes", "+upload-and-analyze", "--file", filepath.Join(t.TempDir(), "missing"), "--yes"); err == nil {
|
|
t.Fatal("upload-and-analyze invalid file accepted")
|
|
}
|
|
if _, _, err := runMinutesAlignmentCLI(t, &minutesE2ECaller{}, "minutes", "+upload-and-analyze", "--file", filepath.Join(t.TempDir(), "missing"), "--dry-run"); err == nil {
|
|
t.Fatal("upload-and-analyze invalid dry-run file accepted")
|
|
}
|
|
|
|
resume := &minutesE2ECaller{responses: map[string][]string{
|
|
"minutes/get_minutes_basic_info": {`{"success":true,"result":{"taskUuid":"u1","title":"done"}}`},
|
|
"minutes/create_mind_graph": {`{"success":true,"result":{}}`},
|
|
"minutes/query_mind_graph_status": {`{"success":true,"result":{"taskStatus":1,"mindGraph":"ready"}}`},
|
|
"minutes/create_speaker_summary": {`{"success":true,"result":{"taskId":"job","status":"processing"}}`},
|
|
"minutes/get_speaker_summary": {`{"success":true,"result":{"summaries":[{"speaker":"a","summary":"b"}]}}`},
|
|
}}
|
|
payload, _, err := runMinutesAlignmentCLI(t, resume, "minutes", "+upload-and-analyze", "--resume-id", "u1", "--artifacts", "basic", "--mindmap", "--speaker-insights", "--yes")
|
|
if err != nil || payload["complete"] != true || payload["taskUuid"] != "u1" {
|
|
t.Fatalf("resume payload=%#v err=%v", payload, err)
|
|
}
|
|
|
|
partial := &minutesE2ECaller{responses: map[string][]string{
|
|
"minutes/get_minutes_basic_info": {`{"success":true,"result":{"taskUuid":"u1","title":"done"}}`},
|
|
"minutes/create_mind_graph": {`{"success":true,"result":{}}`},
|
|
"minutes/query_mind_graph_status": {`{"success":true,"result":{"taskStatus":2}}`},
|
|
"minutes/create_speaker_summary": {`{"success":true,"result":{"taskId":"job","status":"processing"}}`},
|
|
"minutes/get_speaker_summary": {`{"success":false,"errorMsg":"denied"}`},
|
|
}}
|
|
payload, output, err := runMinutesAlignmentCLI(t, partial, "minutes", "+upload-and-analyze", "--resume-id", "u1", "--artifacts", "basic", "--mindmap", "--speaker-insights", "--yes")
|
|
if err == nil || output == "" || payload["complete"] != false || payload["recovery"] == nil {
|
|
t.Fatalf("partial payload=%#v err=%v", payload, err)
|
|
}
|
|
|
|
file := filepath.Join(t.TempDir(), "source.wav")
|
|
if err := os.WriteFile(file, []byte("audio"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
testseam.Swap(t, &minutesPutFile, func(context.Context, string, string, int64) (localio.UploadResult, error) {
|
|
return localio.UploadResult{SizeBytes: 5, Attempts: 1}, nil
|
|
})
|
|
upload := &minutesE2ECaller{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":"u2"}}`},
|
|
"minutes/get_minutes_basic_info": {
|
|
`{"success":true,"result":{"taskUuid":"u2","title":"uploaded"}}`,
|
|
`{"success":true,"result":{"taskUuid":"u2","title":"uploaded"}}`,
|
|
},
|
|
}}
|
|
payload, _, err = runMinutesAlignmentCLI(t, upload, "minutes", "+upload-and-analyze", "--file", file, "--artifacts", "basic", "--yes")
|
|
if err != nil || payload["taskUuid"] != "u2" || payload["complete"] != true {
|
|
t.Fatalf("fresh upload payload=%#v err=%v", payload, err)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageMinutesMindmapBranchesE2E(t *testing.T) {
|
|
if err := runMinutesAlignmentCLIWithWriter(t, &minutesE2ECaller{}, minutesFailWriter{}, "minutes", "+mindmap", "--id", "u1", "--dry-run"); err == nil {
|
|
t.Fatal("mindmap dry-run output failure accepted")
|
|
}
|
|
cases := []struct {
|
|
name string
|
|
responses map[string][]string
|
|
failAt map[string]int
|
|
args []string
|
|
}{
|
|
{name: "create call", failAt: map[string]int{"minutes/create_mind_graph": 1}},
|
|
{name: "poll call", responses: map[string][]string{"minutes/create_mind_graph": {`{}`}}, failAt: map[string]int{"minutes/query_mind_graph_status": 1}},
|
|
{name: "poll parse", responses: map[string][]string{"minutes/create_mind_graph": {`{}`}, "minutes/query_mind_graph_status": {`{"success":true,"result":{}}`}}},
|
|
{name: "timeout", responses: map[string][]string{"minutes/create_mind_graph": {`{}`}, "minutes/query_mind_graph_status": {`{"success":true,"result":{"taskStatus":0}}`}}},
|
|
{name: "resume", responses: map[string][]string{"minutes/query_mind_graph_status": {`{"success":true,"result":{"taskStatus":1}}`}}, args: []string{"--resume"}},
|
|
}
|
|
for _, test := range cases {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
args := []string{"minutes", "+mindmap", "--id", "u1", "--timeout", "1", "--interval", "1", "--yes"}
|
|
args = append(args, test.args...)
|
|
payload, output, err := runMinutesAlignmentCLI(t, &minutesE2ECaller{responses: test.responses, failAt: test.failAt}, args...)
|
|
if test.name == "resume" {
|
|
if err != nil || payload["complete"] != true {
|
|
t.Fatalf("resume payload=%#v err=%v", payload, err)
|
|
}
|
|
} else if err == nil || output == "" {
|
|
t.Fatalf("failure accepted payload=%#v err=%v", payload, err)
|
|
}
|
|
})
|
|
}
|
|
outputFail := &minutesE2ECaller{responses: map[string][]string{
|
|
"minutes/query_mind_graph_status": {`{"success":true,"result":{"taskStatus":1}}`},
|
|
}}
|
|
if err := runMinutesAlignmentCLIWithWriter(t, outputFail, minutesFailWriter{}, "minutes", "+mindmap", "--id", "u1", "--resume", "--yes"); err == nil {
|
|
t.Fatal("mindmap output failure accepted")
|
|
}
|
|
|
|
caller := &minutesE2ECaller{responses: map[string][]string{"minutes/query_mind_graph_status": {`{"success":true,"result":{"taskStatus":0}}`}}}
|
|
helpers.InitDepsForTest(t, caller)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
cmd := &cobra.Command{Use: "mindmap"}
|
|
cmd.SetContext(ctx)
|
|
rt := shortcut.RuntimeContextForTest(cmd, Mindmap)
|
|
if _, err := runMinutesMindmap(rt, "u1", 2*time.Hour, time.Hour, false); err == nil {
|
|
t.Fatal("mindmap cancelled wait accepted")
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageMinutesSpeakerInsightsBranchesE2E(t *testing.T) {
|
|
if err := runMinutesAlignmentCLIWithWriter(t, &minutesE2ECaller{}, minutesFailWriter{}, "minutes", "+speaker-insights", "--id", "u1", "--dry-run"); err == nil {
|
|
t.Fatal("speaker dry-run output failure accepted")
|
|
}
|
|
cases := []struct {
|
|
name string
|
|
responses map[string][]string
|
|
failAt map[string]int
|
|
args []string
|
|
}{
|
|
{name: "create call", failAt: map[string]int{"minutes/create_speaker_summary": 1}},
|
|
{name: "create parse", responses: map[string][]string{"minutes/create_speaker_summary": {`{"success":true,"result":{}}`}}},
|
|
{name: "poll nonpending", responses: map[string][]string{"minutes/create_speaker_summary": {`{"success":true,"result":{"taskId":"job","status":"processing"}}`}, "minutes/get_speaker_summary": {`{"success":false,"errorMsg":"denied"}`}}},
|
|
{name: "poll parse", responses: map[string][]string{"minutes/create_speaker_summary": {`{"success":true,"result":{"taskId":"job","status":"processing"}}`}, "minutes/get_speaker_summary": {`{"success":true,"result":{}}`}}},
|
|
{name: "timeout", responses: map[string][]string{"minutes/create_speaker_summary": {`{"success":true,"result":{"taskId":"job","status":"processing"}}`}, "minutes/get_speaker_summary": {`{"success":false,"errorMsg":"processing"}`}}},
|
|
{name: "resume", responses: map[string][]string{"minutes/get_speaker_summary": {`{"success":true,"result":{"summaries":[{"speaker":"a","summary":"b"}]}}`}}, args: []string{"--resume", "--task-id", "job"}},
|
|
}
|
|
for _, test := range cases {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
args := []string{"minutes", "+speaker-insights", "--id", "u1", "--timeout", "1", "--interval", "1", "--yes"}
|
|
args = append(args, test.args...)
|
|
payload, output, err := runMinutesAlignmentCLI(t, &minutesE2ECaller{responses: test.responses, failAt: test.failAt}, args...)
|
|
if test.name == "resume" {
|
|
if err != nil || payload["complete"] != true || payload["taskId"] != "job" {
|
|
t.Fatalf("resume payload=%#v err=%v", payload, err)
|
|
}
|
|
} else if err == nil || output == "" {
|
|
t.Fatalf("failure accepted payload=%#v err=%v", payload, err)
|
|
}
|
|
})
|
|
}
|
|
outputFail := &minutesE2ECaller{responses: map[string][]string{"minutes/get_speaker_summary": {`{"success":true,"result":{"summaries":[{"speaker":"a","summary":"b"}]}}`}}}
|
|
if err := runMinutesAlignmentCLIWithWriter(t, outputFail, minutesFailWriter{}, "minutes", "+speaker-insights", "--id", "u1", "--resume", "--yes"); err == nil {
|
|
t.Fatal("speaker output failure accepted")
|
|
}
|
|
|
|
for _, message := range []string{"query empty", "processing", "not ready", "result is empty", "business error: code 000", "暂无"} {
|
|
if !speakerSummaryPending(errors.New(message)) {
|
|
t.Fatalf("pending message rejected: %q", message)
|
|
}
|
|
}
|
|
if speakerSummaryPending(nil) || speakerSummaryPending(errors.New("denied")) {
|
|
t.Fatal("non-pending speaker error accepted")
|
|
}
|
|
|
|
caller := &minutesE2ECaller{responses: map[string][]string{"minutes/get_speaker_summary": {`{"success":false,"errorMsg":"processing"}`}}}
|
|
helpers.InitDepsForTest(t, caller)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
cmd := &cobra.Command{Use: "speaker"}
|
|
cmd.SetContext(ctx)
|
|
rt := shortcut.RuntimeContextForTest(cmd, SpeakerInsights)
|
|
if _, err := runMinutesSpeakerInsights(rt, "u1", 2*time.Hour, time.Hour, false, "job"); err == nil {
|
|
t.Fatal("speaker cancelled wait accepted")
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageMinutesPrepareASRBranchesE2E(t *testing.T) {
|
|
if err := runMinutesAlignmentCLIWithWriter(t, &minutesE2ECaller{}, minutesFailWriter{}, "minutes", "+prepare-asr", "--words", "a", "--dry-run"); err == nil {
|
|
t.Fatal("ASR dry-run output failure accepted")
|
|
}
|
|
readFailure := &minutesE2ECaller{failAt: map[string]int{"minutes/list_my_hotwords": 1}}
|
|
if _, _, err := runMinutesAlignmentCLI(t, readFailure, "minutes", "+prepare-asr", "--words", "a", "--yes"); err == nil {
|
|
t.Fatal("ASR current read failure accepted")
|
|
}
|
|
parseFailure := &minutesE2ECaller{responses: map[string][]string{"minutes/list_my_hotwords": {`{"success":true,"result":{}}`}}}
|
|
if _, _, err := runMinutesAlignmentCLI(t, parseFailure, "minutes", "+prepare-asr", "--words", "a", "--yes"); err == nil {
|
|
t.Fatal("ASR current parse failure accepted")
|
|
}
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
responses map[string][]string
|
|
failAt map[string]int
|
|
}{
|
|
{name: "add call", responses: map[string][]string{"minutes/list_my_hotwords": {`{"success":true,"result":{"hotWordList":[]}}`}}, failAt: map[string]int{"minutes/add_personal_hot_word": 1}},
|
|
{name: "add ack", responses: map[string][]string{"minutes/list_my_hotwords": {`{"success":true,"result":{"hotWordList":[]}}`}, "minutes/add_personal_hot_word": {`{"result":{}}`}}},
|
|
{name: "delete call", responses: map[string][]string{"minutes/list_my_hotwords": {`{"success":true,"result":{"hotWordList":["old"]}}`}}, failAt: map[string]int{"minutes/delete_personal_hotword": 1}},
|
|
{name: "delete ack", responses: map[string][]string{"minutes/list_my_hotwords": {`{"success":true,"result":{"hotWordList":["old"]}}`}, "minutes/delete_personal_hotword": {`{"result":{}}`}}},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
args := []string{"minutes", "+prepare-asr", "--words", "a", "--yes"}
|
|
if strings.HasPrefix(test.name, "delete") {
|
|
args = []string{"minutes", "+prepare-asr", "--words", "a", "--sync", "--yes"}
|
|
test.responses["minutes/add_personal_hot_word"] = []string{`{"success":true,"result":{}}`}
|
|
}
|
|
payload, output, err := runMinutesAlignmentCLI(t, &minutesE2ECaller{responses: test.responses, failAt: test.failAt}, args...)
|
|
if err == nil || output == "" || payload["complete"] != false {
|
|
t.Fatalf("ASR failure accepted payload=%#v err=%v", payload, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
verifyCall := &minutesE2ECaller{
|
|
responses: map[string][]string{"minutes/list_my_hotwords": {`{"success":true,"result":{"hotWordList":["a"]}}`}},
|
|
failAt: map[string]int{"minutes/list_my_hotwords": 2},
|
|
}
|
|
payload, output, err := runMinutesAlignmentCLI(t, verifyCall, "minutes", "+prepare-asr", "--words", "a", "--yes")
|
|
if err == nil || output == "" || payload["complete"] != false {
|
|
t.Fatalf("ASR verify call payload=%#v err=%v", payload, err)
|
|
}
|
|
verifyCallOutput := &minutesE2ECaller{
|
|
responses: map[string][]string{"minutes/list_my_hotwords": {`{"success":true,"result":{"hotWordList":["a"]}}`}},
|
|
failAt: map[string]int{"minutes/list_my_hotwords": 2},
|
|
}
|
|
if err := runMinutesAlignmentCLIWithWriter(t, verifyCallOutput, minutesFailWriter{}, "minutes", "+prepare-asr", "--words", "a", "--yes"); err == nil {
|
|
t.Fatal("ASR verify read output failure accepted")
|
|
}
|
|
verifyParse := &minutesE2ECaller{responses: map[string][]string{"minutes/list_my_hotwords": {
|
|
`{"success":true,"result":{"hotWordList":["a"]}}`, `{"success":true,"result":{}}`,
|
|
}}}
|
|
if _, _, err := runMinutesAlignmentCLI(t, verifyParse, "minutes", "+prepare-asr", "--words", "a", "--yes"); err == nil {
|
|
t.Fatal("ASR verify parse failure accepted")
|
|
}
|
|
verifyMismatch := &minutesE2ECaller{responses: map[string][]string{"minutes/list_my_hotwords": {
|
|
`{"success":true,"result":{"hotWordList":["a","old"]}}`, `{"success":true,"result":{"hotWordList":["old"]}}`,
|
|
}, "minutes/delete_personal_hotword": {`{"success":true,"result":{}}`}}}
|
|
payload, output, err = runMinutesAlignmentCLI(t, verifyMismatch, "minutes", "+prepare-asr", "--words", "a", "--sync", "--yes")
|
|
if err == nil || output == "" || payload["verified"] != false {
|
|
t.Fatalf("ASR mismatch payload=%#v err=%v", payload, err)
|
|
}
|
|
syncSuccess := &minutesE2ECaller{responses: map[string][]string{
|
|
"minutes/list_my_hotwords": {`{"success":true,"result":{"hotWordList":["old"]}}`, `{"success":true,"result":{"hotWordList":["a"]}}`},
|
|
"minutes/add_personal_hot_word": {`{"success":true,"result":{}}`},
|
|
"minutes/delete_personal_hotword": {`{"success":true,"result":{}}`},
|
|
}}
|
|
payload, _, err = runMinutesAlignmentCLI(t, syncSuccess, "minutes", "+prepare-asr", "--words", "a", "--sync", "--yes")
|
|
if err != nil || payload["complete"] != true {
|
|
t.Fatalf("ASR sync payload=%#v err=%v", payload, err)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageMinutesPermissionLedgerBranchesE2E(t *testing.T) {
|
|
many := make([]string, 51)
|
|
for index := range many {
|
|
many[index] = fmt.Sprintf("m%d", index)
|
|
}
|
|
if payload, output, err := runMinutesAlignmentCLI(t, &minutesE2ECaller{}, "minutes", "+share", "--id", "u1", "--member-uids", strings.Join(many, ","), "--permission", "view", "--yes"); err == nil || payload != nil || output != "" {
|
|
t.Fatalf("too many members accepted payload=%#v err=%v", payload, err)
|
|
}
|
|
if err := runMinutesAlignmentCLIWithWriter(t, &minutesE2ECaller{}, minutesFailWriter{}, "minutes", "+unshare", "--id", "u1", "--member-uids", "m1", "--dry-run"); err == nil {
|
|
t.Fatal("unshare dry-run output failure accepted")
|
|
}
|
|
unshare := &minutesE2ECaller{responses: map[string][]string{"minutes/remove_member_permission": {`{"success":true,"result":{}}`}}}
|
|
payload, _, err := runMinutesAlignmentCLI(t, unshare, "minutes", "+unshare", "--id", "u1", "--member-uids", "m1", "--yes")
|
|
if err != nil || payload["complete"] != true || payload["succeeded"] != float64(1) {
|
|
t.Fatalf("unshare payload=%#v err=%v", payload, err)
|
|
}
|
|
share := &minutesE2ECaller{responses: map[string][]string{"minutes/add_member_permission": {`{"success":true,"result":{}}`}}}
|
|
payload, _, err = runMinutesAlignmentCLI(t, share, "minutes", "+share", "--id", "u1", "--member-uids", "m1", "--permission", "edit", "--cover", "--sub-resources", "Summary,Note", "--yes")
|
|
if err != nil || payload["complete"] != true {
|
|
t.Fatalf("share payload=%#v err=%v", payload, err)
|
|
}
|
|
args := share.arguments["minutes/add_member_permission"][0]
|
|
if args["coverPermission"] != "true" || len(args["roleSubResourceIds"].([]string)) != 2 {
|
|
t.Fatalf("share args=%#v", args)
|
|
}
|
|
continueFailure := &minutesE2ECaller{responses: map[string][]string{"minutes/remove_member_permission": {`{"result":{}}`, `{"success":true,"result":{}}`}}}
|
|
payload, output, err := runMinutesAlignmentCLI(t, continueFailure, "minutes", "+unshare", "--id", "u1", "--member-uids", "m1,m2", "--failure-policy", "continue", "--yes")
|
|
if err == nil || output == "" || payload["failed"] != float64(1) || payload["succeeded"] != float64(1) {
|
|
t.Fatalf("continue ledger payload=%#v err=%v", payload, err)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageMinutesArtifactCollectorBranches(t *testing.T) {
|
|
success := &minutesE2ECaller{responses: map[string][]string{
|
|
"minutes/get_minutes_basic_info": {`{"success":true,"result":{"taskUuid":"u1","title":"ok"}}`},
|
|
"minutes/get_minutes_ai_summary": {`{"success":true,"result":{"fullSummary":"summary"}}`},
|
|
"minutes/get_minutes_keywords": {`{"success":true,"result":{"keywords":[]}}`},
|
|
"minutes/get_minutes_transcription": {`{"success":true,"result":{"paragraphList":[{"paragraphId":"p1"}],"hasNext":false}}`},
|
|
"minutes/list_minutes_todos": {`{"success":true,"result":{"actions":[]}}`},
|
|
}}
|
|
helpers.InitDepsForTest(t, success)
|
|
rt := shortcut.RuntimeContextForTest(&cobra.Command{Use: "collect"}, ExportPack)
|
|
bundle, failures := collectMinutesArtifactsOnce(rt, "u1", append(minutesWorkflowArtifacts, "unknown"), 10)
|
|
if len(bundle) != 5 || len(failures) != 1 {
|
|
t.Fatalf("collector bundle=%#v failures=%#v", bundle, failures)
|
|
}
|
|
|
|
for _, artifact := range []string{"basic", "summary", "keywords", "transcript", "todos"} {
|
|
t.Run("call "+artifact, func(t *testing.T) {
|
|
tool := map[string]string{
|
|
"basic": "get_minutes_basic_info", "summary": "get_minutes_ai_summary", "keywords": "get_minutes_keywords",
|
|
"transcript": "get_minutes_transcription", "todos": "list_minutes_todos",
|
|
}[artifact]
|
|
caller := &minutesE2ECaller{failAt: map[string]int{"minutes/" + tool: 1}}
|
|
helpers.InitDepsForTest(t, caller)
|
|
rt := shortcut.RuntimeContextForTest(&cobra.Command{Use: "collect"}, ExportPack)
|
|
got, failed := collectMinutesArtifactsOnce(rt, "u1", []string{artifact}, 1)
|
|
if len(got) != 0 || len(failed) != 1 {
|
|
t.Fatalf("got=%#v failed=%#v", got, failed)
|
|
}
|
|
})
|
|
t.Run("parse "+artifact, func(t *testing.T) {
|
|
tool := map[string]string{
|
|
"basic": "get_minutes_basic_info", "summary": "get_minutes_ai_summary", "keywords": "get_minutes_keywords",
|
|
"transcript": "get_minutes_transcription", "todos": "list_minutes_todos",
|
|
}[artifact]
|
|
caller := &minutesE2ECaller{responses: map[string][]string{"minutes/" + tool: {`{"success":true,"result":{}}`}}}
|
|
helpers.InitDepsForTest(t, caller)
|
|
rt := shortcut.RuntimeContextForTest(&cobra.Command{Use: "collect"}, ExportPack)
|
|
got, failed := collectMinutesArtifactsOnce(rt, "u1", []string{artifact}, 1)
|
|
if len(got) != 0 || len(failed) != 1 {
|
|
t.Fatalf("got=%#v failed=%#v", got, failed)
|
|
}
|
|
})
|
|
}
|
|
emptySummary := &minutesE2ECaller{responses: map[string][]string{"minutes/get_minutes_ai_summary": {`{"success":true,"result":{"fullSummary":""}}`}}}
|
|
helpers.InitDepsForTest(t, emptySummary)
|
|
rt = shortcut.RuntimeContextForTest(&cobra.Command{Use: "collect"}, ExportPack)
|
|
if _, failures = collectMinutesArtifactsOnce(rt, "u1", []string{"summary"}, 1); len(failures) != 1 {
|
|
t.Fatalf("empty summary failures=%#v", failures)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageMinutesArtifactWaitAndOutput(t *testing.T) {
|
|
success := &minutesE2ECaller{responses: map[string][]string{"minutes/get_minutes_basic_info": {`{"success":true,"result":{"taskUuid":"u1"}}`}}}
|
|
helpers.InitDepsForTest(t, success)
|
|
rt := shortcut.RuntimeContextForTest(&cobra.Command{Use: "wait"}, ExportPack)
|
|
if _, failures, attempts := waitMinutesArtifacts(rt, "u1", []string{"basic"}, 1, time.Second, 0); len(failures) != 0 || attempts != 1 {
|
|
t.Fatalf("success failures=%#v attempts=%d", failures, attempts)
|
|
}
|
|
failing := &minutesE2ECaller{failAt: map[string]int{"minutes/get_minutes_basic_info": 1}}
|
|
helpers.InitDepsForTest(t, failing)
|
|
rt = shortcut.RuntimeContextForTest(&cobra.Command{Use: "wait"}, ExportPack)
|
|
if _, failures, attempts := waitMinutesArtifacts(rt, "u1", []string{"basic"}, 1, 0, 0); len(failures) != 1 || attempts != 1 {
|
|
t.Fatalf("timeout failures=%#v attempts=%d", failures, attempts)
|
|
}
|
|
cancelled := &minutesE2ECaller{responses: map[string][]string{"minutes/get_minutes_basic_info": {`{"success":true,"result":{}}`}}}
|
|
helpers.InitDepsForTest(t, cancelled)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
cmd := &cobra.Command{Use: "wait"}
|
|
cmd.SetContext(ctx)
|
|
rt = shortcut.RuntimeContextForTest(cmd, ExportPack)
|
|
if _, failures, _ := waitMinutesArtifacts(rt, "u1", []string{"basic"}, 1, 2*time.Hour, time.Hour); len(failures) != 2 || failures[1]["artifact"] != "wait" {
|
|
t.Fatalf("cancel failures=%#v", failures)
|
|
}
|
|
cmd = &cobra.Command{Use: "wait"}
|
|
cmd.SetContext(context.Background())
|
|
rt = shortcut.RuntimeContextForTest(cmd, ExportPack)
|
|
if err := waitMinutesInterval(rt, 0); err != nil {
|
|
t.Fatalf("timer wait: %v", err)
|
|
}
|
|
|
|
cmd.SetOut(minutesFailWriter{})
|
|
if err := outputWorkflowResult(rt, map[string]any{"complete": true}, false, "", ""); err == nil {
|
|
t.Fatal("workflow output failure accepted")
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageMinutesExportPackBranchesE2E(t *testing.T) {
|
|
withCWD := func(t *testing.T) string {
|
|
t.Helper()
|
|
old, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
work := t.TempDir()
|
|
if err := os.Chdir(work); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = os.Chdir(old) })
|
|
return work
|
|
}
|
|
basicCaller := func() *minutesE2ECaller {
|
|
return &minutesE2ECaller{responses: map[string][]string{"minutes/get_minutes_basic_info": {`{"success":true,"result":{"taskUuid":"u1","title":"ok"}}`}}}
|
|
}
|
|
|
|
t.Run("collect failure", func(t *testing.T) {
|
|
withCWD(t)
|
|
payload, output, err := runMinutesAlignmentCLI(t, &minutesE2ECaller{responses: map[string][]string{"minutes/get_minutes_basic_info": {`{"success":true,"result":{}}`}}}, "minutes", "+export-pack", "--id", "u1", "--output", "pack", "--artifacts", "basic")
|
|
if err == nil || output == "" || payload["published"] != false {
|
|
t.Fatalf("collect payload=%#v err=%v", payload, err)
|
|
}
|
|
})
|
|
t.Run("target exists", func(t *testing.T) {
|
|
work := withCWD(t)
|
|
if err := os.Mkdir(filepath.Join(work, "pack"), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, err := runMinutesAlignmentCLI(t, basicCaller(), "minutes", "+export-pack", "--id", "u1", "--output", "pack", "--artifacts", "basic"); err == nil {
|
|
t.Fatal("existing export accepted")
|
|
}
|
|
})
|
|
t.Run("mkdir temp", func(t *testing.T) {
|
|
withCWD(t)
|
|
testseam.Swap(t, &minutesMkdirTemp, func(string, string) (string, error) { return "", errors.New("mkdir temp") })
|
|
if _, _, err := runMinutesAlignmentCLI(t, basicCaller(), "minutes", "+export-pack", "--id", "u1", "--output", "pack", "--artifacts", "basic"); err == nil {
|
|
t.Fatal("mkdir temp failure accepted")
|
|
}
|
|
})
|
|
t.Run("summary write", func(t *testing.T) {
|
|
withCWD(t)
|
|
testseam.Swap(t, &minutesWriteFile, func(string, []byte, os.FileMode) error { return errors.New("write") })
|
|
caller := &minutesE2ECaller{responses: map[string][]string{"minutes/get_minutes_ai_summary": {`{"success":true,"result":{"fullSummary":"summary"}}`}}}
|
|
if _, _, err := runMinutesAlignmentCLI(t, caller, "minutes", "+export-pack", "--id", "u1", "--output", "pack", "--artifacts", "summary"); err == nil {
|
|
t.Fatal("summary write failure accepted")
|
|
}
|
|
})
|
|
t.Run("json write", func(t *testing.T) {
|
|
withCWD(t)
|
|
testseam.Swap(t, &minutesWriteFile, func(string, []byte, os.FileMode) error { return errors.New("write") })
|
|
if _, _, err := runMinutesAlignmentCLI(t, basicCaller(), "minutes", "+export-pack", "--id", "u1", "--output", "pack", "--artifacts", "basic"); err == nil {
|
|
t.Fatal("JSON write failure accepted")
|
|
}
|
|
})
|
|
t.Run("stat", func(t *testing.T) {
|
|
withCWD(t)
|
|
testseam.Swap(t, &minutesStat, func(string) (os.FileInfo, error) { return nil, errors.New("stat") })
|
|
if _, _, err := runMinutesAlignmentCLI(t, basicCaller(), "minutes", "+export-pack", "--id", "u1", "--output", "pack", "--artifacts", "basic"); err == nil {
|
|
t.Fatal("stat failure accepted")
|
|
}
|
|
})
|
|
for _, test := range []struct {
|
|
name string
|
|
responses map[string][]string
|
|
failAt map[string]int
|
|
download func(context.Context, string, localio.DownloadOptions) (localio.DownloadResult, error)
|
|
wantError bool
|
|
}{
|
|
{name: "media call", failAt: map[string]int{"minutes/query_minutes_audio_url": 1}, wantError: true},
|
|
{name: "media parse", responses: map[string][]string{"minutes/query_minutes_audio_url": {`{"success":true,"result":{}}`}}, wantError: true},
|
|
{name: "media download", responses: map[string][]string{"minutes/query_minutes_audio_url": {`{"success":true,"result":{"downloadUrl":"https://example.invalid/a.mp3"}}`}}, download: func(context.Context, string, localio.DownloadOptions) (localio.DownloadResult, error) {
|
|
return localio.DownloadResult{}, errors.New("download")
|
|
}, wantError: true},
|
|
{name: "media success", responses: map[string][]string{"minutes/query_minutes_audio_url": {`{"success":true,"result":{"downloadUrl":"https://example.invalid/a.mp3"}}`}}, download: func(_ context.Context, _ string, options localio.DownloadOptions) (localio.DownloadResult, error) {
|
|
path := filepath.Join(options.BaseDir, "media", "u1.mp3")
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
|
return localio.DownloadResult{}, err
|
|
}
|
|
if err := os.WriteFile(path, []byte("media"), 0o600); err != nil {
|
|
return localio.DownloadResult{}, err
|
|
}
|
|
return localio.DownloadResult{RelativePath: "media/u1.mp3", SizeBytes: 5}, nil
|
|
}},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
withCWD(t)
|
|
if test.download != nil {
|
|
testseam.Swap(t, &minutesDownload, test.download)
|
|
}
|
|
responses := map[string][]string{"minutes/get_minutes_basic_info": {`{"success":true,"result":{"taskUuid":"u1"}}`}}
|
|
for key, values := range test.responses {
|
|
responses[key] = values
|
|
}
|
|
payload, _, err := runMinutesAlignmentCLI(t, &minutesE2ECaller{responses: responses, failAt: test.failAt}, "minutes", "+export-pack", "--id", "u1", "--output", "pack", "--artifacts", "basic", "--include-media")
|
|
if (err != nil) != test.wantError {
|
|
t.Fatalf("payload=%#v err=%v", payload, err)
|
|
}
|
|
if !test.wantError && payload["published"] != true {
|
|
t.Fatalf("payload=%#v", payload)
|
|
}
|
|
})
|
|
}
|
|
t.Run("manifest write", func(t *testing.T) {
|
|
withCWD(t)
|
|
realWrite := minutesWriteFile
|
|
testseam.Swap(t, &minutesWriteFile, func(path string, data []byte, mode os.FileMode) error {
|
|
if filepath.Base(path) == "manifest.json" {
|
|
return errors.New("manifest")
|
|
}
|
|
return realWrite(path, data, mode)
|
|
})
|
|
if _, _, err := runMinutesAlignmentCLI(t, basicCaller(), "minutes", "+export-pack", "--id", "u1", "--output", "pack", "--artifacts", "basic"); err == nil {
|
|
t.Fatal("manifest failure accepted")
|
|
}
|
|
})
|
|
t.Run("rename", func(t *testing.T) {
|
|
withCWD(t)
|
|
testseam.Swap(t, &minutesRename, func(string, string) error { return errors.New("rename") })
|
|
if _, _, err := runMinutesAlignmentCLI(t, basicCaller(), "minutes", "+export-pack", "--id", "u1", "--output", "pack", "--artifacts", "basic"); err == nil {
|
|
t.Fatal("rename failure accepted")
|
|
}
|
|
})
|
|
t.Run("output", func(t *testing.T) {
|
|
withCWD(t)
|
|
if err := runMinutesAlignmentCLIWithWriter(t, basicCaller(), minutesFailWriter{}, "minutes", "+export-pack", "--id", "u1", "--output", "pack", "--artifacts", "basic"); err == nil {
|
|
t.Fatal("export output failure accepted")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCrossPlatformCoverageMinutesExportPathAndJSONFaults(t *testing.T) {
|
|
if _, _, _, err := prepareExportTarget("../escape"); err == nil {
|
|
t.Fatal("unsafe target accepted")
|
|
}
|
|
for _, test := range []struct {
|
|
name string
|
|
run func(*testing.T)
|
|
}{
|
|
{name: "getwd", run: func(t *testing.T) {
|
|
testseam.Swap(t, &minutesGetwd, func() (string, error) { return "", errors.New("getwd") })
|
|
if _, _, _, err := prepareExportTarget("pack"); err == nil {
|
|
t.Fatal("getwd failure accepted")
|
|
}
|
|
}},
|
|
{name: "eval", run: func(t *testing.T) {
|
|
testseam.Swap(t, &minutesEvalSymlinks, func(string) (string, error) { return "", errors.New("eval") })
|
|
if _, _, _, err := prepareExportTarget("pack"); err == nil {
|
|
t.Fatal("eval failure accepted")
|
|
}
|
|
}},
|
|
{name: "rel", run: func(t *testing.T) {
|
|
testseam.Swap(t, &minutesRel, func(string, string) (string, error) { return "", errors.New("rel") })
|
|
if _, _, _, err := prepareExportTarget("pack"); err == nil {
|
|
t.Fatal("rel failure accepted")
|
|
}
|
|
}},
|
|
{name: "escape", run: func(t *testing.T) {
|
|
testseam.Swap(t, &minutesRel, func(string, string) (string, error) { return "../escape", nil })
|
|
if _, _, _, err := prepareExportTarget("pack"); err == nil {
|
|
t.Fatal("rel escape accepted")
|
|
}
|
|
}},
|
|
{name: "target lstat", run: func(t *testing.T) {
|
|
testseam.Swap(t, &minutesLstat, func(string) (os.FileInfo, error) { return nil, errors.New("lstat") })
|
|
if _, _, _, err := prepareExportTarget("pack"); err == nil {
|
|
t.Fatal("lstat failure accepted")
|
|
}
|
|
}},
|
|
{name: "parent", run: func(t *testing.T) {
|
|
testseam.Swap(t, &minutesRel, func(string, string) (string, error) { return "parent/pack", nil })
|
|
testseam.Swap(t, &minutesLstat, func(string) (os.FileInfo, error) { return nil, errors.New("parent") })
|
|
if _, _, _, err := prepareExportTarget("pack"); err == nil {
|
|
t.Fatal("unsafe parent accepted")
|
|
}
|
|
}},
|
|
} {
|
|
t.Run(test.name, test.run)
|
|
}
|
|
|
|
base := t.TempDir()
|
|
if err := ensureExportParent(base, "."); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Run("mkdir", func(t *testing.T) {
|
|
testseam.Swap(t, &minutesLstat, func(string) (os.FileInfo, error) { return nil, os.ErrNotExist })
|
|
testseam.Swap(t, &minutesMkdir, func(string, os.FileMode) error { return errors.New("mkdir") })
|
|
if err := ensureExportParent(base, "a"); err == nil {
|
|
t.Fatal("mkdir failure accepted")
|
|
}
|
|
})
|
|
t.Run("lstat", func(t *testing.T) {
|
|
testseam.Swap(t, &minutesLstat, func(string) (os.FileInfo, error) { return nil, errors.New("lstat") })
|
|
if err := ensureExportParent(base, "a"); err == nil {
|
|
t.Fatal("lstat failure accepted")
|
|
}
|
|
})
|
|
t.Run("file", func(t *testing.T) {
|
|
path := filepath.Join(base, "file")
|
|
if err := os.WriteFile(path, []byte("x"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := ensureExportParent(base, "file/child"); err == nil {
|
|
t.Fatal("file parent accepted")
|
|
}
|
|
})
|
|
t.Run("create", func(t *testing.T) {
|
|
if err := ensureExportParent(base, "created/nested"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
|
|
t.Run("marshal", func(t *testing.T) {
|
|
testseam.Swap(t, &minutesMarshalIndent, func(any, string, string) ([]byte, error) { return nil, errors.New("marshal") })
|
|
if err := writeJSONFile(filepath.Join(t.TempDir(), "x.json"), map[string]any{}); err == nil {
|
|
t.Fatal("marshal failure accepted")
|
|
}
|
|
})
|
|
t.Run("write", func(t *testing.T) {
|
|
testseam.Swap(t, &minutesWriteFile, func(string, []byte, os.FileMode) error { return errors.New("write") })
|
|
if err := writeJSONFile(filepath.Join(t.TempDir(), "x.json"), map[string]any{}); err == nil {
|
|
t.Fatal("write failure accepted")
|
|
}
|
|
})
|
|
path := filepath.Join(t.TempDir(), "ok.json")
|
|
if err := writeJSONFile(path, map[string]any{"ok": true}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil || !bytes.HasSuffix(raw, []byte("\n")) || !json.Valid(raw) {
|
|
t.Fatalf("JSON output=%q err=%v", raw, err)
|
|
}
|
|
}
|