//go:build skill_verify // +build skill_verify // Package skill_e2e_test runs every backtick `dws ...` command from skill // docs through a real Cobra binary in --mock mode and rejects unknown-command // and unknown-flag dispatch failures. Mock JSON envelopes and help-like // non-JSON output are both accepted because some documented entries // intentionally name a command group rather than a leaf operation. // // Build tag rationale: this test depends on a real `dws` binary built from the // current checkout and invokes hundreds of subprocesses. Default `go test // ./...` skips it. Build with `go build -o dws ./cmd`, then run explicitly // with: `go test -tags skill_verify ./test/skill_e2e/...` // // This is Layer B of the skill verification matrix — pure open-source, // no real tenant required. Layer A (test/skill_static) checks the current // source command tree and multi-skill flags; this layer actually invokes each // supported command end-to-end. // // Why --mock instead of real calls: open-source CI has no real DingTalk // tenant, and depending on one would either (a) leak credentials or // (b) make the test flaky on tenant drift. --mock returns a deterministic // shape (`{"_mock": true, "_tool": "", "result": [], "success": true}`) // for Cobra commands that resolve through the MCP path. Unknown command/flag // failures surface here exactly as they would on a real tenant. package skill_e2e_test import ( "bufio" "encoding/json" "fmt" "io/fs" "os" "os/exec" "path/filepath" "regexp" "runtime" "strings" "sync" "testing" ) var ( backtickRe = regexp.MustCompile("`(dws\\s+[^`]+?)`") quotedRe = regexp.MustCompile(`"[^"]*"`) // Sub-paths intentionally referenced as anti-pattern guidance. antiPatternAllowlist = map[string]bool{ "dws calendar list": true, "dws minutes detail": true, "dws minutes info": true, "dws minutes summary": true, "dws minutes transcription": true, "dws report inbox": true, "dws skill add": true, "dws skill find": true, } // Line markers indicating an anti-pattern context (whole line skipped). antiPatternLineMarkers = []string{ "[禁止]", "【禁止】", "禁止使用", "禁止编造", "❌", "错误写法", "高频错误", "反例", "不存在", "不应该", "不要写", "不要用", "反模式", "错例", "该命令不存在", "不是合法", "unknown flag", "LLM 高频幻觉", "臆造", "虚构", "编造", "缺少子命令", "缺少 scope", "不识别", "不认识", "不存在此", "不是顶层", "应该是", "应为", "参数名是", "参数错", "应该用", "废弃", "已下线", } // Verbs that imply writes; auto-append --dry-run. writeVerbs = map[string]bool{ "create": true, "update": true, "delete": true, "send": true, "recall": true, "approve": true, "reject": true, "revoke": true, "commit": true, "mkdir": true, "add": true, "remove": true, "set": true, "invite": true, "transfer": true, "quit": true, "rename": true, "move": true, "copy": true, "append": true, "insert": true, "merge": true, "unmerge": true, "write": true, "replace": true, "mute": true, "cancel": true, "install": true, "publish": true, "forward": true, "reply": true, } // Utility commands do not participate in the MCP --mock envelope path; // Layer A still validates their Cobra registration. Executing them here can // trigger local setup, authentication, upgrades, or long-running consumers. mockUnsupportedRoots = map[string]bool{ "api": true, "auth": true, "catalog": true, "completion": true, "config": true, "doctor": true, "event": true, "mcp": true, "plugin": true, "profile": true, "schema": true, "skill": true, "upgrade": true, "version": true, } ) func repoRoot(t *testing.T) string { t.Helper() _, here, _, _ := runtime.Caller(0) return filepath.Clean(filepath.Join(filepath.Dir(here), "..", "..")) } func dwsBinary(t *testing.T, root string) string { t.Helper() if configured := os.Getenv("DWS_TEST_BINARY"); configured != "" { p, err := filepath.Abs(configured) if err != nil { t.Fatalf("resolve DWS_TEST_BINARY: %v", err) } if info, err := os.Stat(p); err != nil || info.IsDir() { t.Fatalf("DWS_TEST_BINARY must name an executable file: %s", p) } return p } name := "dws" if runtime.GOOS == "windows" { name += ".exe" } p := filepath.Join(root, name) if info, err := os.Stat(p); err == nil && !info.IsDir() { return p } t.Skipf("current-checkout dws binary not found; run `go build -o %s ./cmd` or set DWS_TEST_BINARY", name) return "" } func appendMultiReferenceMarkdown(files []string, multiRoot string) ([]string, error) { err := filepath.WalkDir(multiRoot, func(path string, d fs.DirEntry, walkErr error) error { if walkErr != nil { return walkErr } if d.IsDir() || !strings.EqualFold(filepath.Ext(path), ".md") { return nil } rel, err := filepath.Rel(multiRoot, path) if err != nil { return err } parts := strings.Split(filepath.ToSlash(rel), "/") if len(parts) >= 3 && parts[1] == "references" { files = append(files, path) } return nil }) return files, err } type cmdRef struct { File string Line int Cmd string } func extract(root string) ([]cmdRef, error) { dir := filepath.Join(root, "skills") var files []string for _, pat := range []string{ dir + "/mono/SKILL.md", dir + "/mono/references/products/*.md", dir + "/multi/*/SKILL.md", } { m, err := filepath.Glob(pat) if err != nil { return nil, err } files = append(files, m...) } var err error files, err = appendMultiReferenceMarkdown(files, filepath.Join(dir, "multi")) if err != nil { return nil, err } var refs []cmdRef for _, f := range files { out, err := os.ReadFile(f) if err != nil { return nil, err } text := string(out) lineNo := 0 scanner := bufio.NewScanner(strings.NewReader(text)) scanner.Buffer(make([]byte, 1024*1024), 1024*1024*4) for scanner.Scan() { lineNo++ line := scanner.Text() if isAntiLine(line) { continue } for _, m := range backtickRe.FindAllStringSubmatch(line, -1) { cmd := strings.TrimSpace(m[1]) if shouldSkip(cmd) { continue } refs = append(refs, cmdRef{File: f, Line: lineNo, Cmd: cmd}) } } } return refs, nil } func isAntiLine(line string) bool { for _, marker := range antiPatternLineMarkers { if strings.Contains(line, marker) { return true } } return false } func shouldSkip(cmd string) bool { tokens := strings.Fields(cmd) if len(tokens) >= 2 && mockUnsupportedRoots[tokens[1]] { return true } if cmd == "dws sheet export" || strings.HasPrefix(cmd, "dws sheet export ") { // The export helper performs its asynchronous polling loop even under // --mock; Layer A validates its command path without making this audit // wait for the production export timeout. return true } if strings.HasPrefix(cmd, "dws dev ") || cmd == "dws dev" { // dev (product id: devapp) is a target MCP-discovered command tree. Until the MCP product // is published into the open-source registry, validate its Agent routing // through test/skill_tests.md rather than mock cobra dispatch. return true } if strings.Contains(cmd, "[flags]") || strings.Contains(cmd, "[command]") || strings.Contains(cmd, "[--") { return true } if strings.Contains(cmd, "|") || strings.Contains(cmd, "$(") || strings.Contains(cmd, " & ") { return true } if strings.Contains(cmd, "...") || strings.Contains(cmd, " > ") { return true } // Sub-path must not contain placeholder syntax. subPart := cmd if idx := strings.Index(cmd, " --"); idx >= 0 { subPart = cmd[:idx] } // Drop quoted strings before scanning subPart = quotedRe.ReplaceAllString(subPart, "__Q__") for _, t := range strings.Fields(subPart) { if strings.HasPrefix(t, "<") || strings.Contains(t, "/") || strings.Contains(t, "*") { return true } } // Placeholders with spaces inside angle brackets confuse word-based parsing for _, m := range regexp.MustCompile(`<[^>]*>`).FindAllString(cmd, -1) { if strings.Contains(m, " ") { return true } } return false } func subPath(cmd string) string { cmd2 := quotedRe.ReplaceAllString(cmd, "__Q__") var tokens []string for _, t := range strings.Fields(cmd2) { if strings.HasPrefix(t, "-") { break } tokens = append(tokens, t) } return strings.Join(tokens, " ") } func isWrite(sub string) bool { tokens := strings.Fields(sub) for i, t := range tokens { if i == 0 { continue } if writeVerbs[t] { return true } // Compound names like "send-by-bot", "recall-by-bot" etc. for w := range writeVerbs { if strings.HasPrefix(t, w+"-") { return true } } } return false } // substitute replaces well-known placeholders with safe dummy values. func substitute(cmd string) string { subst := map[string]string{ "": "test", "": "test", "<群名>": "test", "<姓名>": "wukong01", "<标题>": "_test", "<内容>": "_test", "": "2026-05-21", "": "2026-05-21", "": "2026-05-14T00:00:00+08:00", "": "2026-05-21T23:59:59+08:00", "": "2026-05-21T10:00:00+08:00", "": "userDummy", "": "cidDummy==", "": "cidDummy==", "": "msgDummy==", "": "Dummy", "": "rcDummy", "": "rcDummy", "": "tk", "": "1", "": "1", "": "PROC_d", "": "INST_d", "": "0123456789abcdef0123456789abcdef", "": "d", "": "sp", "": "ws", "": "n", "": "Sheet1", "": "b", "": "t", "": "f", "": "1", "": "1", "": "k", "": "x@y.z", "": "x@y.z", "": "x@y.z", "": "primary", "": "e", "": "rows", "": "0", "": "1", "": "OWNER", "": "mine", "": "myWikiSpace", "": "xlsx", "": "A1:B2", "": "n", "": "Sheet1", "": "b", "": "t", "": "f", "": "n", "": "1", "": "r", "": "TASK_d", "": "/tmp/test.txt", "": "/tmp/test.txt", } for k, v := range subst { cmd = strings.ReplaceAll(cmd, k, v) } // Catch-all for any remaining return regexp.MustCompile(`<[^>]+>`).ReplaceAllString(cmd, "dummy") } func TestSkillCommandsMockDispatch(t *testing.T) { root := repoRoot(t) dws := dwsBinary(t, root) t.Setenv("HOME", t.TempDir()) t.Setenv("DWS_CONFIG_DIR", t.TempDir()) t.Setenv("DWS_DISABLE_KEYCHAIN", "1") t.Setenv("DWS_KEYCHAIN_DIR", t.TempDir()) refs, err := extract(root) if err != nil { t.Fatalf("extract: %v", err) } // Dedupe by exact command text. seen := map[string]bool{} var unique []cmdRef for _, r := range refs { if antiPatternAllowlist[subPath(r.Cmd)] || seen[r.Cmd] { continue } seen[r.Cmd] = true unique = append(unique, r) } t.Logf("Extracted %d refs, %d unique runnable commands", len(refs), len(unique)) type failure struct { ref cmdRef actual string reason string } var fails []failure var mu sync.Mutex var wg sync.WaitGroup semaphore := make(chan struct{}, 8) // parallelism cap for _, r := range unique { r := r wg.Add(1) semaphore <- struct{}{} go func() { defer wg.Done() defer func() { <-semaphore }() actual := substitute(r.Cmd) if !strings.Contains(actual, " --mock") { actual += " --mock" } if !strings.Contains(actual, " --format") { actual += " --format json" } if isWrite(subPath(r.Cmd)) && !strings.Contains(actual, " --dry-run") { actual += " --dry-run" } argv := rejoinQuoted(actual) out, _ := exec.Command(dws, argv[1:]...).CombinedOutput() s := string(out) if strings.Contains(s, "unknown flag") || strings.Contains(s, "unknown command") || strings.Contains(s, "unknown subcommand") { mu.Lock() fails = append(fails, failure{ref: r, actual: actual, reason: firstLine(s, 200)}) mu.Unlock() return } // Parse JSON; must have either _mock=true or success-marker. var j map[string]any if err := json.Unmarshal(out, &j); err != nil { // Non-JSON output is fine for help-like commands; only flag a fail if // stderr/exit-code indicates dispatch error (we already caught unknown above). return } if mock, ok := j["_mock"].(bool); ok && mock { return // PASS: mocked dispatch worked } // Some commands (helper-overrides) don't proxy through mock; accept success markers too. if v, ok := j["success"]; ok && (v == true || v == "true") { return } if errBlock, ok := j["error"].(map[string]any); ok { msg, _ := errBlock["message"].(string) if strings.Contains(msg, "unknown flag") || strings.Contains(msg, "unknown command") || strings.Contains(msg, "unknown subcommand") { mu.Lock() fails = append(fails, failure{ref: r, actual: actual, reason: msg}) mu.Unlock() } } }() } wg.Wait() if len(fails) == 0 { t.Logf("✓ All %d unique skill commands dispatch cleanly under --mock", len(unique)) return } var sb strings.Builder sb.WriteString(fmt.Sprintf("Found %d skill commands that fail at cobra/envelope dispatch under --mock:\n\n", len(fails))) for _, f := range fails { rel, _ := filepath.Rel(root, f.ref.File) sb.WriteString(fmt.Sprintf(" ❌ %s:%d\n", rel, f.ref.Line)) sb.WriteString(fmt.Sprintf(" cmd: %s\n", f.ref.Cmd)) sb.WriteString(fmt.Sprintf(" actual: %s\n", f.actual)) sb.WriteString(fmt.Sprintf(" reason: %s\n\n", f.reason)) } t.Fatal(sb.String()) } func firstLine(s string, max int) string { if i := strings.Index(s, "\n"); i >= 0 { s = s[:i] } if len(s) > max { s = s[:max] } return s } // rejoinQuoted is a tiny shell-tokenizer that keeps "double-quoted" runs as one arg. func rejoinQuoted(s string) []string { var argv []string var cur strings.Builder inQ := false for i := 0; i < len(s); i++ { c := s[i] switch c { case '"': inQ = !inQ case ' ', '\t': if inQ { cur.WriteByte(c) continue } if cur.Len() > 0 { argv = append(argv, cur.String()) cur.Reset() } default: cur.WriteByte(c) } } if cur.Len() > 0 { argv = append(argv, cur.String()) } return argv }