fix(schema): validate agent example delivery
This commit is contained in:
1 parent
f6c2ce655d
commit
55afc656ac
29 files changed
+2266
-413
No files matched your search
@@ -15,6 +15,7 @@ package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -22,30 +23,105 @@ import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
var manualAgentExamplePlaceholderPattern = regexp.MustCompile(`<([^>]+)>`)
|
||||
|
||||
// TestManualAgentExamplesDryRun executes every reviewed example through the
|
||||
// real Cobra command and argument-validation path with global --dry-run forced
|
||||
// on. The test is opt-in because it is intentionally exhaustive; normal CI
|
||||
// already validates every path and flag in-process. No shell is involved and
|
||||
// HOME is isolated, so examples cannot expand shell syntax or consume a
|
||||
// developer's local DWS configuration.
|
||||
// TestManualAgentExamplesContract is the always-on gate. It validates every
|
||||
// example, including contract_only entries, against the live bound Cobra path,
|
||||
// flags, required arguments, constraints, and final typed safety.
|
||||
func TestManualAgentExamplesContract(t *testing.T) {
|
||||
plan := manualAgentExampleExecutionPlan(t)
|
||||
if plan.Total == 0 {
|
||||
t.Fatal("no reviewed Agent examples were contract validated")
|
||||
}
|
||||
t.Logf("Agent example contract: total=%d dry_run=%d contract_only=%d", plan.Total, plan.DryRun, plan.ContractOnly)
|
||||
}
|
||||
|
||||
// TestManualAgentExamplesDryRun first validates every reviewed example against
|
||||
// its real BoundCommand, Cobra required arguments, and final typed constraints.
|
||||
// It then executes only the deterministic dry_run subset. Final typed
|
||||
// confirmation and exact reviewed manual dispositions account for the stable
|
||||
// contract_only subset; runtime failures never create implicit skips. No shell
|
||||
// is involved and HOME is isolated.
|
||||
func TestManualAgentExamplesDryRun(t *testing.T) {
|
||||
if os.Getenv("DWS_AGENT_EXAMPLES_DRY_RUN") != "1" {
|
||||
t.Skip("set DWS_AGENT_EXAMPLES_DRY_RUN=1 to execute every reviewed Agent example through Cobra --dry-run")
|
||||
}
|
||||
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
sandboxRoot := t.TempDir()
|
||||
homeDir := filepath.Join(sandboxRoot, "home")
|
||||
configDir := filepath.Join(sandboxRoot, "config")
|
||||
for _, dir := range []string{homeDir, configDir} {
|
||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||
t.Fatalf("create isolated test directory %s: %v", dir, err)
|
||||
}
|
||||
}
|
||||
t.Setenv("HOME", homeDir)
|
||||
t.Setenv("DWS_CONFIG_DIR", configDir)
|
||||
t.Setenv("HTTP_PROXY", "http://127.0.0.1:1")
|
||||
t.Setenv("HTTPS_PROXY", "http://127.0.0.1:1")
|
||||
t.Setenv("NO_PROXY", "")
|
||||
files := newManualAgentExampleFiles(t)
|
||||
|
||||
plan := manualAgentExampleExecutionPlan(t)
|
||||
if plan.Total == 0 {
|
||||
t.Fatal("no reviewed Agent examples were contract validated")
|
||||
}
|
||||
t.Chdir(sandboxRoot)
|
||||
files := newManualAgentExampleFiles(t, sandboxRoot)
|
||||
|
||||
executed := 0
|
||||
for _, execution := range plan.Examples {
|
||||
if execution.Mode == cli.ManualAgentExampleModeContractOnly {
|
||||
continue
|
||||
}
|
||||
execution := execution
|
||||
t.Run(fmt.Sprintf("%s/%d", strings.ReplaceAll(execution.CanonicalPath, ".", "/"), execution.Index), func(t *testing.T) {
|
||||
argv, err := cli.ParseManualAgentExampleArgv(execution.Example)
|
||||
if err != nil {
|
||||
t.Fatalf("parse example %q: %v", execution.Example, err)
|
||||
}
|
||||
args := materializeManualAgentExampleArgv(argv[1:], files)
|
||||
if !manualAgentExampleHasFlag(args, "dry-run") {
|
||||
args = append([]string{"--dry-run"}, args...)
|
||||
}
|
||||
|
||||
capture, err := executeManualAgentExampleCapture(t, args)
|
||||
if capture.ToolCallAttempts != 0 {
|
||||
t.Fatalf("eligible dry-run attempted %d ToolCaller invocation(s)\nsource: %s\nargv: %q\noutput:\n%s", capture.ToolCallAttempts, execution.Example, args, capture.Output)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("dry-run example failed: %v\nsource: %s\nargv: %q\noutput:\n%s", err, execution.Example, args, capture.Output)
|
||||
}
|
||||
if !manualAgentExampleDryRunObserved(capture.Output, capture.DryRunChecks) {
|
||||
t.Fatalf("example returned without audited dry-run evidence (caller dry-run checks: %d)\nsource: %s\nargv: %q\noutput:\n%s", capture.DryRunChecks, execution.Example, args, capture.Output)
|
||||
}
|
||||
executed++
|
||||
})
|
||||
}
|
||||
if executed != plan.DryRun {
|
||||
t.Fatalf("executed dry_run examples = %d, plan requires %d", executed, plan.DryRun)
|
||||
}
|
||||
t.Logf("Agent examples: total=%d dry_run=%d contract_only=%d typed_safety=%d reviewed_manual=%d", plan.Total, plan.DryRun, plan.ContractOnly, plan.TypedSafetyContractOnly, plan.ReviewedContractOnly)
|
||||
reasonCodes := make([]string, 0, len(plan.ContractOnlyByReason))
|
||||
for reasonCode := range plan.ContractOnlyByReason {
|
||||
reasonCodes = append(reasonCodes, string(reasonCode))
|
||||
}
|
||||
sort.Strings(reasonCodes)
|
||||
for _, reasonCode := range reasonCodes {
|
||||
t.Logf("Agent examples contract_only[%s]=%d", reasonCode, plan.ContractOnlyByReason[cli.ManualAgentExampleReasonCode(reasonCode)])
|
||||
}
|
||||
}
|
||||
|
||||
func manualAgentExampleExecutionPlan(t testing.TB) cli.ManualAgentExampleExecutionPlan {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile("../cli/schema_manual_hints.json")
|
||||
if err != nil {
|
||||
t.Fatalf("read reviewed Manual Agent hints: %v", err)
|
||||
@@ -54,85 +130,122 @@ func TestManualAgentExamplesDryRun(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeManualSchemaHintSource() error = %v", err)
|
||||
}
|
||||
|
||||
canonicals := make([]string, 0, len(snapshot.AgentHints.Tools))
|
||||
for canonical := range snapshot.AgentHints.Tools {
|
||||
canonicals = append(canonicals, canonical)
|
||||
contractRoot := NewRootCommand()
|
||||
if _, err := cli.ApplyEmbeddedManualSchemaHints(contractRoot); err != nil {
|
||||
t.Fatalf("ApplyEmbeddedManualSchemaHints() error = %v", err)
|
||||
}
|
||||
sort.Strings(canonicals)
|
||||
|
||||
validated := 0
|
||||
for _, canonical := range canonicals {
|
||||
for index, example := range snapshot.AgentHints.Tools[canonical].Examples {
|
||||
canonical, index, example := canonical, index, example
|
||||
t.Run(fmt.Sprintf("%s/%d", strings.ReplaceAll(canonical, ".", "/"), index), func(t *testing.T) {
|
||||
argv, err := cli.ParseManualAgentExampleArgv(example)
|
||||
if err != nil {
|
||||
t.Fatalf("parse example %q: %v", example, err)
|
||||
}
|
||||
args := materializeManualAgentExampleArgv(argv[1:], files)
|
||||
if !manualAgentExampleHasFlag(args, "dry-run") {
|
||||
args = append([]string{"--dry-run"}, args...)
|
||||
}
|
||||
|
||||
output, err := executeManualAgentExampleCapture(t, args)
|
||||
if err != nil {
|
||||
t.Fatalf("dry-run example failed: %v\nsource: %s\nargv: %q\noutput:\n%s", err, example, args, output)
|
||||
}
|
||||
if !manualAgentExampleDryRunObserved(output) {
|
||||
t.Fatalf("example returned without dry-run evidence\nsource: %s\nargv: %q\noutput:\n%s", example, args, output)
|
||||
}
|
||||
validated++
|
||||
})
|
||||
}
|
||||
effective, err := cli.BuildEffectiveCommandRegistry(contractRoot)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildEffectiveCommandRegistry() error = %v", err)
|
||||
}
|
||||
if validated == 0 {
|
||||
t.Fatal("no reviewed Agent examples were executed")
|
||||
bound, err := cli.BindEffectiveCommandRegistry(contractRoot, effective)
|
||||
if err != nil {
|
||||
t.Fatalf("BindEffectiveCommandRegistry() error = %v", err)
|
||||
}
|
||||
t.Logf("executed %d reviewed Agent examples through real Cobra --dry-run", validated)
|
||||
registry, err := cli.AssembleSchemaRegistryFromBound(bound)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleSchemaRegistryFromBound() error = %v", err)
|
||||
}
|
||||
plan, err := cli.BuildManualAgentExampleExecutionPlan(bound, registry, snapshot.AgentHints)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildManualAgentExampleExecutionPlan() error = %v", err)
|
||||
}
|
||||
return plan
|
||||
}
|
||||
|
||||
func executeManualAgentExampleCapture(t testing.TB, args []string) (string, error) {
|
||||
type manualAgentExampleCapture struct {
|
||||
Output string
|
||||
DryRunChecks int64
|
||||
ToolCallAttempts int64
|
||||
}
|
||||
|
||||
type manualAgentExampleFailClosedCaller struct {
|
||||
dryRunChecks atomic.Int64
|
||||
toolCallAttempts atomic.Int64
|
||||
}
|
||||
|
||||
func (c *manualAgentExampleFailClosedCaller) CallTool(_ context.Context, productID, toolName string, _ map[string]any) (*edition.ToolResult, error) {
|
||||
c.toolCallAttempts.Add(1)
|
||||
return nil, fmt.Errorf("real ToolCaller invocation blocked during Agent example dry-run: %s/%s", productID, toolName)
|
||||
}
|
||||
|
||||
func (c *manualAgentExampleFailClosedCaller) Format() string { return "json" }
|
||||
|
||||
func (c *manualAgentExampleFailClosedCaller) DryRun() bool {
|
||||
c.dryRunChecks.Add(1)
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *manualAgentExampleFailClosedCaller) Fields() string { return "" }
|
||||
func (c *manualAgentExampleFailClosedCaller) JQ() string { return "" }
|
||||
|
||||
func executeManualAgentExampleCapture(t testing.TB, args []string) (manualAgentExampleCapture, error) {
|
||||
t.Helper()
|
||||
oldArgs := os.Args
|
||||
os.Args = append([]string{"dws"}, args...)
|
||||
defer func() { os.Args = oldArgs }()
|
||||
|
||||
oldStdout, oldStderr := os.Stdout, os.Stderr
|
||||
readPipe, writePipe, err := os.Pipe()
|
||||
oldColorOutput, oldColorError := color.Output, color.Error
|
||||
captureFile, err := os.CreateTemp(t.TempDir(), "agent-example-output-*.log")
|
||||
if err != nil {
|
||||
t.Fatalf("open output capture pipe: %v", err)
|
||||
t.Fatalf("open output capture file: %v", err)
|
||||
}
|
||||
os.Stdout, os.Stderr = writePipe, writePipe
|
||||
defer captureFile.Close()
|
||||
os.Stdout, os.Stderr = captureFile, captureFile
|
||||
color.Output, color.Error = captureFile, captureFile
|
||||
defer func() {
|
||||
os.Stdout, os.Stderr = oldStdout, oldStderr
|
||||
color.Output, color.Error = oldColorOutput, oldColorError
|
||||
}()
|
||||
|
||||
root := NewRootCommand()
|
||||
originalCaller := helpers.GetCaller()
|
||||
auditCaller := &manualAgentExampleFailClosedCaller{}
|
||||
helpers.InitDeps(auditCaller)
|
||||
defer helpers.InitDeps(originalCaller)
|
||||
var output bytes.Buffer
|
||||
root.SetOut(&output)
|
||||
root.SetErr(&output)
|
||||
root.SetArgs(args)
|
||||
execErr := root.Execute()
|
||||
|
||||
_ = writePipe.Close()
|
||||
os.Stdout, os.Stderr = oldStdout, oldStderr
|
||||
captured, readErr := io.ReadAll(readPipe)
|
||||
_ = readPipe.Close()
|
||||
if readErr != nil {
|
||||
t.Fatalf("read output capture pipe: %v", readErr)
|
||||
color.Output, color.Error = oldColorOutput, oldColorError
|
||||
if _, err := captureFile.Seek(0, io.SeekStart); err != nil {
|
||||
t.Fatalf("rewind output capture file: %v", err)
|
||||
}
|
||||
return output.String() + string(captured), execErr
|
||||
captured, readErr := io.ReadAll(captureFile)
|
||||
if readErr != nil {
|
||||
t.Fatalf("read output capture file: %v", readErr)
|
||||
}
|
||||
return manualAgentExampleCapture{
|
||||
Output: output.String() + string(captured),
|
||||
DryRunChecks: auditCaller.dryRunChecks.Load(),
|
||||
ToolCallAttempts: auditCaller.toolCallAttempts.Load(),
|
||||
}, execErr
|
||||
}
|
||||
|
||||
type manualAgentExampleFiles struct {
|
||||
root string
|
||||
markdown string
|
||||
json string
|
||||
batch string
|
||||
binary string
|
||||
image string
|
||||
}
|
||||
|
||||
func newManualAgentExampleFiles(t testing.TB) manualAgentExampleFiles {
|
||||
func newManualAgentExampleFiles(t testing.TB, root string) manualAgentExampleFiles {
|
||||
t.Helper()
|
||||
root := t.TempDir()
|
||||
markdown := filepath.Join(root, "content.md")
|
||||
jsonFile := filepath.Join(root, "report.json")
|
||||
batch := filepath.Join(root, "styles.json")
|
||||
binary := filepath.Join(root, "report.pdf")
|
||||
image := filepath.Join(root, "chart.png")
|
||||
for path, content := range map[string][]byte{
|
||||
markdown: []byte("# Agent dry-run fixture\n\nNo business call is allowed.\n"),
|
||||
jsonFile: []byte(`[{"content":"Agent dry-run fixture","sort":"0","key":"fixture","contentType":"markdown","type":"1"}]`),
|
||||
batch: []byte(`[{"sheetId":"Sheet1","range":"A1:B2","fontWeight":"bold"}]`),
|
||||
binary: []byte("%PDF-1.4\n%%EOF\n"),
|
||||
image: {0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n'},
|
||||
} {
|
||||
@@ -140,7 +253,7 @@ func newManualAgentExampleFiles(t testing.TB) manualAgentExampleFiles {
|
||||
t.Fatalf("write dry-run fixture %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
return manualAgentExampleFiles{root: root, markdown: markdown, binary: binary, image: image}
|
||||
return manualAgentExampleFiles{root: root, markdown: markdown, json: jsonFile, batch: batch, binary: binary, image: image}
|
||||
}
|
||||
|
||||
func materializeManualAgentExampleArgv(argv []string, files manualAgentExampleFiles) []string {
|
||||
@@ -184,8 +297,14 @@ func materializeManualAgentExampleArgv(argv []string, files manualAgentExampleFi
|
||||
} else {
|
||||
replacement = files.binary
|
||||
}
|
||||
case "content-file", "contents-file":
|
||||
case "content-file":
|
||||
replacement = files.markdown
|
||||
case "contents-file":
|
||||
replacement = files.json
|
||||
case "batch":
|
||||
if strings.HasSuffix(strings.ToLower(value), "styles.json") {
|
||||
replacement = files.batch
|
||||
}
|
||||
case "output":
|
||||
if value == "." || value == "" {
|
||||
replacement = files.root
|
||||
@@ -223,10 +342,77 @@ func manualAgentExampleHasFlag(argv []string, target string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func manualAgentExampleDryRunObserved(output string) bool {
|
||||
func manualAgentExampleDryRunObserved(output string, auditedDryRunChecks int64) bool {
|
||||
normalized := strings.ToLower(output)
|
||||
return strings.Contains(normalized, "dry-run") ||
|
||||
strings.Contains(normalized, `"dry_run":true`) ||
|
||||
strings.Contains(normalized, `"dry_run": true`) ||
|
||||
(strings.Contains(output, "Tool:") && strings.Contains(output, "Arguments:"))
|
||||
(auditedDryRunChecks > 0 && strings.Contains(output, "操作:"))
|
||||
}
|
||||
|
||||
func TestManualAgentExampleDryRunObservedSupportsNilArguments(t *testing.T) {
|
||||
if !manualAgentExampleDryRunObserved("[DRY-RUN] Preview only, not executed:\nTool: calendar_list", 0) {
|
||||
t.Fatal("dry-run output with a Tool and nil Arguments was not recognized")
|
||||
}
|
||||
if manualAgentExampleDryRunObserved("Tool: calendar_list", 0) {
|
||||
t.Fatal("a Tool line without dry-run evidence must not be accepted")
|
||||
}
|
||||
operationSummary := "操作: 下载钉盘文件\n文件ID: test"
|
||||
if manualAgentExampleDryRunObserved(operationSummary, 0) {
|
||||
t.Fatal("a bare operation summary without an audited caller dry-run check must not be accepted")
|
||||
}
|
||||
if !manualAgentExampleDryRunObserved(operationSummary, 1) {
|
||||
t.Fatal("an operation summary guarded by the injected caller's dry-run check was not recognized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestManualAgentExampleFailClosedCallerRecordsToolCalls(t *testing.T) {
|
||||
caller := &manualAgentExampleFailClosedCaller{}
|
||||
if !caller.DryRun() {
|
||||
t.Fatal("fail-closed caller must advertise dry-run mode")
|
||||
}
|
||||
if _, err := caller.CallTool(context.Background(), "calendar", "list_events", nil); err == nil {
|
||||
t.Fatal("fail-closed caller accepted a ToolCaller invocation")
|
||||
}
|
||||
if got := caller.dryRunChecks.Load(); got != 1 {
|
||||
t.Fatalf("DryRun() checks = %d, want 1", got)
|
||||
}
|
||||
if got := caller.toolCallAttempts.Load(); got != 1 {
|
||||
t.Fatalf("CallTool() attempts = %d, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestManualAgentExampleChatGroupMuteMemberDryRunKeepsUserIDsWithoutToolCall(t *testing.T) {
|
||||
sandboxRoot := t.TempDir()
|
||||
configDir := filepath.Join(sandboxRoot, "config")
|
||||
if err := os.MkdirAll(configDir, 0o700); err != nil {
|
||||
t.Fatalf("create isolated config directory: %v", err)
|
||||
}
|
||||
t.Setenv("HOME", sandboxRoot)
|
||||
t.Setenv("DWS_CONFIG_DIR", configDir)
|
||||
|
||||
capture, err := executeManualAgentExampleCapture(t, []string{
|
||||
"--dry-run",
|
||||
"chat", "group-mute-member",
|
||||
"--group", "test_openConversationId",
|
||||
"--users", "userId1,userId2",
|
||||
"--mute-time", "3600000",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("group-mute-member dry-run failed: %v\noutput:\n%s", err, capture.Output)
|
||||
}
|
||||
if capture.ToolCallAttempts != 0 {
|
||||
t.Fatalf("group-mute-member dry-run attempted %d ToolCaller invocation(s)\noutput:\n%s", capture.ToolCallAttempts, capture.Output)
|
||||
}
|
||||
if capture.DryRunChecks == 0 {
|
||||
t.Fatalf("group-mute-member did not check the injected caller's dry-run state\noutput:\n%s", capture.Output)
|
||||
}
|
||||
for _, expected := range []string{`"uids"`, `"userId1"`, `"userId2"`} {
|
||||
if !strings.Contains(capture.Output, expected) {
|
||||
t.Fatalf("group-mute-member dry-run preview missing %s\noutput:\n%s", expected, capture.Output)
|
||||
}
|
||||
}
|
||||
if strings.Contains(capture.Output, `"openDingTalkIds"`) {
|
||||
t.Fatalf("group-mute-member dry-run unexpectedly resolved user IDs remotely\noutput:\n%s", capture.Output)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user