Files
dingtalk-workspace-cli/internal/helpers/product_examples_test.go

439 lines
13 KiB
Go

package helpers
import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"strings"
"testing"
"time"
"unicode"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
type productExampleCaller struct {
calls int
mode string
dry bool
format string
}
func (c *productExampleCaller) CallTool(_ context.Context, _, _ string, _ map[string]any) (*edition.ToolResult, error) {
c.calls++
switch c.mode {
case "transport-error":
return nil, fmt.Errorf("synthetic tool transport failure")
case "invalid-json":
return &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: "{"}}}, nil
case "empty-content":
return &edition.ToolResult{}, nil
case "non-text":
return &edition.ToolResult{Content: []edition.ContentBlock{{Type: "image"}}}, nil
}
record := map[string]any{
"id": "item-id", "openId": "open-id", "unionId": "union-id", "name": "Example",
"title": "Example", "text": "content", "content": "content", "status": "SUCCESS",
"type": "text", "url": "https://example.invalid/item", "downloadUrl": "https://example.invalid/download",
"createdAt": "2026-01-02T03:04:05Z", "updatedAt": "2026-01-02T03:04:05Z",
"startTime": "2026-01-02T03:04:05Z", "endTime": "2026-01-02T04:04:05Z",
"fileName": "fixture.txt", "dentryId": float64(1), "spaceId": float64(2),
"messageId": "message-id", "docId": "doc-id", "workspaceId": "workspace-id",
"sheetId": "sheet-id", "tableId": "table-id", "baseId": "base-id",
"recordId": "record-id", "taskId": "task-id", "jobId": "job-id",
"eventId": "event-id", "calendarId": "calendar-id", "roomId": "room-id",
"conversationId": "conversation-id", "openConversationId": "cid-item-id",
"processInstanceId": "process-id", "reportId": "report-id", "templateId": "template-id",
"todoId": "todo-id", "mediaId": "media-id", "attachmentId": "attachment-id",
"userId": "user-id", "email": "user@example.com", "code": float64(0), "errcode": float64(0),
}
dataMap := cloneStringAnyMap(record)
for key, value := range map[string]any{
"success": true, "status": "SUCCESS", "taskStatus": "SUCCESS",
"hasMore": false, "nextCursor": "", "cursor": "",
"items": []any{record}, "records": []any{record}, "data": []any{record}, "list": []any{record},
"result": record, "downloadUrl": "https://example.invalid/download", "resourceUrl": "https://example.invalid/resource",
"message": map[string]any{"id": "message-id"},
"emailAccounts": []any{map[string]any{"email": "user@example.com", "type": "PERSONAL"}},
} {
dataMap[key] = value
}
resultMap := cloneStringAnyMap(record)
for key, value := range map[string]any{
"success": true, "status": "SUCCESS", "taskStatus": "SUCCESS", "hasMore": false,
"nextCursor": "", "items": []any{record}, "records": []any{record}, "data": []any{record},
"downloadUrl": "https://example.invalid/download", "resourceUrl": "https://example.invalid/resource",
"uploadKey": "upload-key", "jobId": "job-id", "taskId": "task-id",
"message": map[string]any{"id": "message-id"},
"emailAccounts": []any{map[string]any{"email": "user@example.com", "type": "PERSONAL"}},
} {
resultMap[key] = value
}
payload := map[string]any{
"success": true,
"result": resultMap,
"data": dataMap,
"items": []any{record},
"records": []any{record},
"hasMore": false,
"nextCursor": "",
"status": "SUCCESS",
"taskStatus": "SUCCESS",
"downloadUrl": "https://example.invalid/download",
"resourceUrl": "https://example.invalid/resource",
"presignedUrl": "https://example.invalid/upload",
"uploadKey": "upload-key",
"jobId": "job-id",
"taskId": "task-id",
"messageId": "message-id",
"fileName": "fixture.txt",
"dentryId": float64(1),
"spaceId": float64(2),
"code": float64(0),
"errcode": float64(0),
}
if c.mode == "list-shapes" {
payload["result"] = []any{record}
payload["data"] = []any{record}
}
if c.mode == "flat-record" {
payload = record
}
if c.mode == "nested-data" {
payload = map[string]any{"data": map[string]any{"result": resultMap}}
}
if c.mode == "empty-result" {
payload = map[string]any{}
}
raw, err := json.Marshal(payload)
if err != nil {
return nil, err
}
return &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: string(raw)}}}, nil
}
func (c *productExampleCaller) Format() string {
if c.format == "" {
return "json"
}
return c.format
}
func (c *productExampleCaller) DryRun() bool { return c.dry }
func (*productExampleCaller) Fields() string { return "" }
func (*productExampleCaller) JQ() string { return "" }
func TestCrossPlatformCoverageProductCommandExamplesAreExecutableContracts(t *testing.T) {
workingDirectory, err := os.Getwd()
if err != nil {
t.Fatalf("get working directory: %v", err)
}
if err := os.Chdir(t.TempDir()); err != nil {
t.Fatalf("use isolated working directory: %v", err)
}
t.Cleanup(func() { _ = os.Chdir(workingDirectory) })
previousDeps := deps
previousArgs := os.Args
previousStdin := os.Stdin
previousPut := httpPutFile
previousGet := httpGetFile
previousWhiteboardSleep := whiteboardSleep
t.Cleanup(func() {
deps = previousDeps
os.Args = previousArgs
os.Stdin = previousStdin
httpPutFile = previousPut
httpGetFile = previousGet
whiteboardSleep = previousWhiteboardSleep
})
caller := &productExampleCaller{}
InitDeps(caller)
deps.Out.w = io.Discard
deps.Out.errW = io.Discard
// Product examples execute real RunE paths; whiteboard insert retries must
// not burn the suite timeout on real sleep (race CI uses a 12m package cap).
// Sheet export uses helperAfter for progressive polling, so keep that clock
// deterministic too; context deadlines alone still accumulate heavily under
// Windows coverage instrumentation.
whiteboardSleep = func(time.Duration) {}
testseam.Swap(t, &helperAfter, func(time.Duration) <-chan time.Time {
ch := make(chan time.Time, 1)
ch <- time.Now()
return ch
})
httpPutFile = func(context.Context, string, map[string]string, string, int64) error { return nil }
httpGetFile = func(_ context.Context, _ string, _ map[string]string, destPath string) error {
if destPath == "" {
return nil
}
return os.WriteFile(destPath, []byte("example"), 0o600)
}
confirmationInput := t.TempDir() + "/confirmations.txt"
if err := os.WriteFile(confirmationInput, []byte(strings.Repeat("yes\n", 2048)), 0o600); err != nil {
t.Fatalf("write confirmation input: %v", err)
}
confirmationFile, err := os.Open(confirmationInput)
if err != nil {
t.Fatalf("open confirmation input: %v", err)
}
t.Cleanup(func() { _ = confirmationFile.Close() })
os.Stdin = confirmationFile
commands := NewPublicCommands(&captureRunner{})
executed := 0
malformedExecuted := 0
for _, root := range commands {
installExampleGlobalFlags(root)
root.SilenceErrors = true
root.SilenceUsage = true
root.SetOut(io.Discard)
root.SetErr(io.Discard)
os.Args = []string{"dws", root.Name(), "--yes"}
for _, invocation := range commandExampleInvocations(root) {
executed++
caller.mode = ""
resetCommandFlags(root)
root.SetArgs(invocation)
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
err := root.ExecuteContext(ctx)
cancel()
if isCommandSyntaxError(err) {
t.Errorf("dws %s %s: %v", root.Name(), strings.Join(invocation, " "), err)
}
for _, mode := range []string{"list-shapes", "flat-record", "nested-data", "empty-result", "empty-content", "non-text", "transport-error", "invalid-json"} {
caller.mode = mode
resetCommandFlags(root)
root.SetArgs(invocation)
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
_ = root.ExecuteContext(ctx)
cancel()
}
caller.mode = ""
for _, malformed := range malformedExampleInvocations(invocation) {
malformedExecuted++
resetCommandFlags(root)
root.SetArgs(malformed)
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
_ = root.ExecuteContext(ctx)
cancel()
}
}
}
if executed < 400 {
t.Fatalf("executed %d product examples, want at least 400", executed)
}
if caller.calls < 250 {
t.Fatalf("product examples made %d tool calls, want at least 250", caller.calls)
}
if malformedExecuted < 300 {
t.Fatalf("executed %d malformed example variants, want at least 300", malformedExecuted)
}
}
func malformedExampleInvocations(invocation []string) [][]string {
var variants [][]string
for index, token := range invocation {
if !strings.HasPrefix(token, "--") {
continue
}
if equal := strings.IndexByte(token, '='); equal >= 0 {
variant := append([]string(nil), invocation...)
variant[index] = token[:equal+1] + malformedFlagValue(token[equal+1:])
variants = append(variants, variant)
continue
}
if index+1 >= len(invocation) || strings.HasPrefix(invocation[index+1], "-") {
continue
}
variant := append([]string(nil), invocation...)
variant[index+1] = malformedFlagValue(invocation[index+1])
variants = append(variants, variant)
}
return variants
}
func malformedFlagValue(value string) string {
for _, r := range value {
if r < '0' || r > '9' {
return "invalid"
}
}
return "-1"
}
func commandExampleInvocations(root *cobra.Command) [][]string {
var invocations [][]string
var walk func(*cobra.Command)
walk = func(command *cobra.Command) {
for _, line := range logicalExampleLines(command.Example) {
tokens, err := splitExampleCommand(line)
if err != nil || len(tokens) < 2 || tokens[0] != "dws" || tokens[1] != root.Name() {
continue
}
if unsupportedExampleTokens(tokens) {
continue
}
invocations = append(invocations, tokens[2:])
}
for _, child := range command.Commands() {
walk(child)
}
}
walk(root)
return invocations
}
func logicalExampleLines(example string) []string {
physical := strings.Split(example, "\n")
logical := make([]string, 0, len(physical))
var current string
for _, line := range physical {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
if current != "" {
current += " " + line
} else {
current = line
}
if strings.HasSuffix(current, "\\") {
current = strings.TrimSpace(strings.TrimSuffix(current, "\\"))
continue
}
if index := strings.Index(current, "dws "); index >= 0 {
logical = append(logical, current[index:])
}
current = ""
}
if current != "" {
if index := strings.Index(current, "dws "); index >= 0 {
logical = append(logical, current[index:])
}
}
return logical
}
func splitExampleCommand(line string) ([]string, error) {
var tokens []string
var token strings.Builder
var quote rune
escaped := false
tokenStarted := false
flush := func() {
if tokenStarted {
tokens = append(tokens, token.String())
token.Reset()
tokenStarted = false
}
}
for _, r := range line {
if escaped {
token.WriteRune(r)
tokenStarted = true
escaped = false
continue
}
if r == '\\' && quote != '\'' {
escaped = true
continue
}
if quote != 0 {
if r == quote {
quote = 0
} else {
token.WriteRune(r)
tokenStarted = true
}
continue
}
switch {
case r == '\'' || r == '"':
quote = r
tokenStarted = true
case r == '#':
flush()
return tokens, nil
case unicode.IsSpace(r):
flush()
default:
token.WriteRune(r)
tokenStarted = true
}
}
if escaped || quote != 0 {
return nil, fmt.Errorf("unterminated escape or quote")
}
flush()
return tokens, nil
}
func installExampleGlobalFlags(root *cobra.Command) {
flags := root.PersistentFlags()
if flags.Lookup("format") == nil {
flags.String("format", "json", "test global output format")
}
if flags.Lookup("fields") == nil {
flags.String("fields", "", "test global field projection")
}
if flags.Lookup("jq") == nil {
flags.String("jq", "", "test global jq filter")
}
if flags.Lookup("timeout") == nil {
flags.Int("timeout", 30, "test global timeout")
}
if flags.Lookup("dry-run") == nil {
flags.Bool("dry-run", false, "test global dry-run")
}
if flags.Lookup("yes") == nil {
flags.Bool("yes", false, "test global confirmation bypass")
}
}
func unsupportedExampleTokens(tokens []string) bool {
for _, token := range tokens {
if strings.ContainsAny(token, "|`$") || strings.HasPrefix(token, "[") || strings.HasSuffix(token, "]") {
return true
}
}
return false
}
func resetCommandFlags(command *cobra.Command) {
reset := func(flag *pflag.Flag) {
_ = flag.Value.Set(flag.DefValue)
flag.Changed = false
}
command.Flags().VisitAll(reset)
command.PersistentFlags().VisitAll(reset)
for _, child := range command.Commands() {
resetCommandFlags(child)
}
}
func isCommandSyntaxError(err error) bool {
if err == nil {
return false
}
message := strings.ToLower(err.Error())
for _, fragment := range []string{
"unknown command", "unknown flag", "flag needs an argument", "requires at least",
"requires exactly", "accepts 1 arg", "accepts 2 arg", "required flag",
} {
if strings.Contains(message, fragment) {
return true
}
}
return false
}