Files
dingtalk-workspace-cli/test/cli_compat/helpers_test.go
T
2026-03-27 17:55:57 +08:00

239 lines
6.2 KiB
Go

package cli_compat_test
import (
"bytes"
"encoding/json"
"sync"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/app"
"github.com/spf13/cobra"
)
// ═══════════════════════════════════════════════════════════════
// Backward-compatible shim: matches the old CLI test harness API
// but uses dws NewRootCommand + EchoRunner under the hood.
// ═══════════════════════════════════════════════════════════════
// ToolResult mirrors mcp.ToolResult for test compatibility with the reference test suite.
type ToolResult struct {
Content []ContentBlock `json:"content"`
IsError bool `json:"isError,omitempty"`
}
// ContentBlock mirrors mcp.ContentBlock for test compatibility.
type ContentBlock struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
}
type capturedCall struct {
ToolName string
Args map[string]any
Endpoint string
}
type mcpCallCapture struct {
mu sync.Mutex
calls []capturedCall
dryRun bool
confirm bool
}
func (c *mcpCallCapture) record(name string, args map[string]any, endpoint string) {
c.mu.Lock()
defer c.mu.Unlock()
c.calls = append(c.calls, capturedCall{ToolName: name, Args: args, Endpoint: endpoint})
}
func (c *mcpCallCapture) last() *capturedCall {
c.mu.Lock()
defer c.mu.Unlock()
if len(c.calls) == 0 {
return nil
}
return &c.calls[len(c.calls)-1]
}
func (c *mcpCallCapture) all() []capturedCall {
c.mu.Lock()
defer c.mu.Unlock()
out := make([]capturedCall, len(c.calls))
copy(out, c.calls)
return out
}
// ── per-test capture registry ───────────────────────────────
var (
captureMu sync.Mutex
captures = map[*testing.T]*mcpCallCapture{}
)
func linkCapture(t *testing.T, cap *mcpCallCapture) {
captureMu.Lock()
captures[t] = cap
captureMu.Unlock()
t.Cleanup(func() {
captureMu.Lock()
delete(captures, t)
captureMu.Unlock()
})
}
func getCapture(t *testing.T) *mcpCallCapture {
captureMu.Lock()
defer captureMu.Unlock()
return captures[t]
}
// ── setup ───────────────────────────────────────────────────
func setupTestDeps(t *testing.T, _ string) *mcpCallCapture {
t.Helper()
cap := &mcpCallCapture{}
linkCapture(t, cap)
return cap
}
func setupTestDepsWithDryRun(t *testing.T, product string) *mcpCallCapture {
t.Helper()
cap := setupTestDeps(t, product)
cap.dryRun = true
return cap
}
func setupTestDepsAutoConfirm(t *testing.T, product string) *mcpCallCapture {
t.Helper()
cap := setupTestDeps(t, product)
cap.confirm = true
return cap
}
func buildRoot() *cobra.Command {
return app.NewRootCommand()
}
// ── execution ───────────────────────────────────────────────
func execCmd(t *testing.T, root *cobra.Command, path []string, flags map[string]string) error {
t.Helper()
return execCmdWithArgs(t, root, path, flags, nil)
}
func execCmdWithArgs(t *testing.T, root *cobra.Command, path []string, flags map[string]string, args []string) error {
t.Helper()
cap := getCapture(t)
cliArgs := []string{"-f", "json"}
cliArgs = append(cliArgs, path...)
// Add dry-run if capture says so
if cap != nil && cap.dryRun {
cliArgs = append(cliArgs, "--dry-run")
}
// Add --yes if auto-confirm
if cap != nil && cap.confirm {
cliArgs = append(cliArgs, "--yes")
}
for k, v := range flags {
if v == "" {
continue
}
cliArgs = append(cliArgs, "--"+k, v)
}
cliArgs = append(cliArgs, args...)
var out bytes.Buffer
root.SetOut(&out)
root.SetErr(&bytes.Buffer{})
root.SetArgs(cliArgs)
err := root.Execute()
if err != nil {
return err
}
// Parse EchoRunner output and record into capture
var inv struct {
Invocation struct {
Tool string `json:"tool"`
Params map[string]any `json:"params"`
DryRun bool `json:"dry_run"`
} `json:"invocation"`
}
if cap != nil {
if jsonErr := json.Unmarshal(out.Bytes(), &inv); jsonErr == nil && inv.Invocation.Tool != "" {
if !inv.Invocation.DryRun {
cap.record(inv.Invocation.Tool, inv.Invocation.Params, "")
}
// For dry-run: don't record (matches old behavior: assertCallCount == 0)
}
}
return nil
}
// ── asserts (use the passed *mcpCallCapture directly) ───────
func assertToolName(t *testing.T, cap *mcpCallCapture, expected string) {
t.Helper()
last := cap.last()
if last == nil {
t.Fatalf("expected MCP call to %q, but no calls were captured", expected)
}
if last.ToolName != expected {
t.Errorf("expected tool name %q, got %q", expected, last.ToolName)
}
}
func assertToolArg(t *testing.T, cap *mcpCallCapture, key string, expected any) {
t.Helper()
last := cap.last()
if last == nil {
t.Fatalf("expected MCP call with arg %q=%v, but no calls were captured", key, expected)
}
got, ok := last.Args[key]
if !ok {
t.Errorf("expected arg %q in MCP call, but it was not present. args: %v", key, last.Args)
return
}
gotJSON, _ := json.Marshal(got)
expectedJSON, _ := json.Marshal(expected)
if string(gotJSON) != string(expectedJSON) {
t.Errorf("arg %q: expected %v, got %v", key, string(expectedJSON), string(gotJSON))
}
}
func assertArgNotPresent(t *testing.T, cap *mcpCallCapture, key string) {
t.Helper()
last := cap.last()
if last == nil {
t.Fatalf("no MCP call captured")
}
if _, ok := last.Args[key]; ok {
t.Errorf("expected arg %q to be absent, but it was present: %v", key, last.Args[key])
}
}
func assertNilArgs(t *testing.T, cap *mcpCallCapture) {
t.Helper()
last := cap.last()
if last == nil {
t.Fatal("expected MCP call, but no calls were captured")
}
if len(last.Args) > 0 {
t.Errorf("expected nil/empty args, got %v", last.Args)
}
}
// assertCallCount asserts the number of captured MCP calls.
func assertCallCount(t *testing.T, capture *mcpCallCapture, expected int) {
t.Helper()
calls := capture.all()
if len(calls) != expected {
t.Errorf("expected %d MCP calls, got %d", expected, len(calls))
}
}