Files
dingtalk-workspace-cli/test/skill_static/skill_static_test.go
T
修雨 5ce7cb61b0 feat(skill): dws skill setup with mono/multi modes (#332)
新增 `dws skill setup` 交互式安装命令,支持单产品(mono)和多产品(multi,标记为试验版)两种 skill 布局,并与开源 dws v1.0.30 命令对齐。

主要变更:
- 骨架:skills/ 拆为 skills/mono/ 和 skills/multi/(29 个产品 skill),git rename 保留历史
- 命令:新增 `skill setup` cobra 子命令,支持 --mode、--target、--source、--yes,huh 交互
  - multi 模式支持 -s/--skill、-x/--exclude 按产品选择
  - 覆盖全部 14 个 agent home 目录(与 install.sh 一致)
- 发布:install.sh / install.ps1 / post-goreleaser.sh 改读 skills/mono;dws-skills.zip 同时包含 mono 和 multi 内容;~/.dws 本地缓存
- 内容对齐:删除 8 个仅内部使用的产品 skill、dingtalk-a2a;修正 mono/multi 共 15+ 处命令引用以匹配 OSS dws v1.0.30;chat skill 从 dws-wukong 回移更新
- 漂移清理:物理删除 skill 文档中 OSS 未暴露的命令,true-fail 命令转为 caveat;--help diff 100% 覆盖审计
- 测试:新增 Layer A 静态命令调度验证 + Layer B 端到端 mock 调度验证
- 同步 main:drive 9 个命令同步到 mono/multi skill
2026-05-26 09:17:37 +08:00

345 lines
10 KiB
Go

//go:build skill_verify
// +build skill_verify
// Package skill_static_test verifies that every `dws ...` command example
// referenced in skill documentation can actually be dispatched by the
// open-source dws CLI binary (cobra Available Commands check).
//
// Build tag rationale: this test depends on a real `dws` binary being
// reachable via PATH, which the default `go test ./...` doesn't guarantee.
// Run explicitly with: `go test -tags skill_verify ./test/skill_static/...`
//
// Layer A of the skill verification matrix:
// 1. Every backtick `dws ...` command in skills/**/*.md is parsed.
// 2. For each, the sub-command path (tokens before first --flag) is
// checked against `dws <parent> --help`'s "Available Commands" list.
// 3. Every flag used must appear in the leaf command's help text.
//
// The test deliberately tolerates a small whitelist of intentional
// anti-pattern references (e.g. "dws minutes detail" appearing in the
// 「高频错误命令对照表」 column of a 错例 → 正解 table). Anything else
// must dispatch cleanly.
package skill_static_test
import (
"bufio"
"fmt"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"
"sync"
"testing"
)
var (
backtickCmd = regexp.MustCompile("`(dws\\s+[^`]+?)`")
// Sub-paths intentionally referenced as anti-pattern examples
// (kept in docs to guide LLMs away from common hallucinations).
antiPatternAllowlist = map[string]bool{
"dws calendar list": true, // calendar.md: "CLI doesn't have this; do not invent it"
"dws minutes detail": true, // minutes.md anti-pattern table
"dws minutes info": true, // (top-level, not under `get`)
"dws minutes summary": true,
"dws minutes transcription": true,
"dws report inbox": true, // explicit "禁止编造" warnings
"dws skill add": true, // backward-compat stub, exists but not in Available Commands
"dws skill find": true, // backward-compat stub
}
// Argument-like tokens the parser should not mistake for sub-commands.
dateLikeRe = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}`)
)
func repoRoot(t *testing.T) string {
t.Helper()
_, here, _, _ := runtime.Caller(0)
// test/skill_static/skill_static_test.go → repo root
return filepath.Clean(filepath.Join(filepath.Dir(here), "..", ".."))
}
func dwsBinary(t *testing.T) string {
t.Helper()
p, err := exec.LookPath("dws")
if err != nil {
t.Skipf("dws binary not on PATH; install dws (or run `go build -o /tmp/dws ./cmd` and PATH=/tmp:$PATH) to enable this test")
}
return p
}
type cmdRef struct {
File string
Line int
Cmd string
}
// extractCommands scans skills/**/*.md for every backtick `dws ...` command.
func extractCommands(root string) ([]cmdRef, error) {
skillsDir := filepath.Join(root, "skills")
var refs []cmdRef
files, err := filepath.Glob(skillsDir + "/mono/SKILL.md")
if err != nil {
return nil, err
}
var more []string
if more, err = filepath.Glob(skillsDir + "/mono/references/products/*.md"); err == nil {
files = append(files, more...)
}
if more, err = filepath.Glob(skillsDir + "/mono/references/*.md"); err == nil {
files = append(files, more...)
}
if more, err = filepath.Glob(skillsDir + "/multi/*/SKILL.md"); err == nil {
files = append(files, more...)
}
if more, err = filepath.Glob(skillsDir + "/multi/*/references/*.md"); err == nil {
files = append(files, more...)
}
for _, f := range files {
data, err := readFile(f)
if err != nil {
return nil, err
}
lineNo := 0
scanner := bufio.NewScanner(strings.NewReader(data))
scanner.Buffer(make([]byte, 1024*1024), 1024*1024*4)
for scanner.Scan() {
lineNo++
matches := backtickCmd.FindAllStringSubmatch(scanner.Text(), -1)
for _, m := range matches {
cmd := strings.TrimSpace(m[1])
if shouldSkip(cmd) {
continue
}
refs = append(refs, cmdRef{File: f, Line: lineNo, Cmd: cmd})
}
}
}
return refs, nil
}
func readFile(p string) (string, error) {
b, err := exec.Command("cat", p).Output()
if err != nil {
return "", err
}
return string(b), nil
}
// anglePlaceholderRe matches any <...> placeholder. We check raw cmd because
// placeholders with spaces (e.g. <url 或 dentryUuid>) confuse word-based
// parsing — easier to reject upfront.
var anglePlaceholderRe = regexp.MustCompile(`<[^>]*>`)
func shouldSkip(cmd string) bool {
if strings.Contains(cmd, "[flags]") || strings.Contains(cmd, "[command]") {
return true
}
if strings.Contains(cmd, "|") || strings.Contains(cmd, "$(") || strings.Contains(cmd, " & ") {
return true
}
if strings.Contains(cmd, "...") || strings.Contains(cmd, " > ") {
return true
}
// Markdown optional-flag notation like `[--limit <1-20>]` or `[--yes]`
// confuses word-based parsing. Treat any "[--" as a templated example.
if strings.Contains(cmd, "[--") {
return true
}
// Replace placeholders with a safe token, then check sub-path portion
// for any remaining noise. If the placeholder contains spaces it would
// already have polluted word-based parsing — easier to skip entirely.
if anglePlaceholderRe.MatchString(cmd) {
// Allow only commands where placeholder is entirely inside flag values
// (after first " --"). Sub-path tokens (before first --flag) must be clean.
idx := strings.Index(cmd, " --")
if idx < 0 {
return true // no --flag, but has <>, so whole cmd is templated
}
if anglePlaceholderRe.MatchString(cmd[:idx]) {
return true
}
// Placeholder inside flag values is fine — but if it spans spaces,
// our word-based parser would break. Detect spaces inside any <...>.
for _, m := range anglePlaceholderRe.FindAllString(cmd, -1) {
if strings.Contains(m, " ") {
return true
}
}
}
// Reject "/" or "*" within sub-path tokens.
subPart := cmd
if idx := strings.Index(cmd, " --"); idx >= 0 {
subPart = cmd[:idx]
}
for _, t := range strings.Fields(subPart) {
if strings.Contains(t, "/") || strings.Contains(t, "*") {
return true
}
}
return false
}
// quotedRe matches double-quoted substrings (possibly with spaces inside).
var quotedRe = regexp.MustCompile(`"[^"]*"`)
// parseSubPath returns (sub-path, leaf, flags). sub-path is tokens up to but
// not including the first --flag and any clearly-non-subcommand token.
func parseSubPath(cmd string) (subPath, leaf string, flags []string) {
// Collapse "quoted strings" to a single placeholder so word-split works.
cmd = quotedRe.ReplaceAllString(cmd, "__QUOTED__")
tokens := strings.Fields(cmd)
var subTokens []string
for i := 0; i < len(tokens); i++ {
t := tokens[i]
if strings.HasPrefix(t, "--") || (strings.HasPrefix(t, "-") && len(t) > 1 && !strings.ContainsAny(string(t[1]), "0123456789")) {
// flag, take name before '='
flags = append(flags, strings.SplitN(t, "=", 2)[0])
// skip its value if present
if i+1 < len(tokens) && !strings.HasPrefix(tokens[i+1], "-") {
i++
}
continue
}
if len(subTokens) == 0 && t != "dws" {
break // doesn't start with dws → not a real cmd
}
// Stop sub-path on noise tokens
if dateLikeRe.MatchString(t) || strings.Contains(t, "/") || strings.Contains(t, "*") {
break
}
subTokens = append(subTokens, t)
}
if len(subTokens) < 2 {
return "", "", flags
}
subPath = strings.Join(subTokens, " ")
leaf = subTokens[len(subTokens)-1]
return subPath, leaf, flags
}
// helpCache memoizes `dws <sub> --help` outputs.
type helpCache struct {
mu sync.Mutex
dws string
m map[string]string
}
func newHelpCache(dws string) *helpCache {
return &helpCache{dws: dws, m: map[string]string{}}
}
func (c *helpCache) get(sub string) string {
c.mu.Lock()
defer c.mu.Unlock()
if v, ok := c.m[sub]; ok {
return v
}
args := append(strings.Fields(sub)[1:], "--help")
out, _ := exec.Command(c.dws, args...).CombinedOutput()
c.m[sub] = string(out)
return string(out)
}
var availableRe = regexp.MustCompile(`(?s)Available Commands:\n((?:\s+\S.*\n)+)`)
func parseAvailable(helpText string) []string {
m := availableRe.FindStringSubmatch(helpText)
if m == nil {
return nil
}
var cmds []string
for _, line := range strings.Split(m[1], "\n") {
fields := strings.Fields(line)
if len(fields) > 0 && !strings.HasPrefix(fields[0], "-") {
cmds = append(cmds, fields[0])
}
}
return cmds
}
func TestSkillCommandsDispatch(t *testing.T) {
root := repoRoot(t)
dws := dwsBinary(t)
cache := newHelpCache(dws)
refs, err := extractCommands(root)
if err != nil {
t.Fatalf("extract: %v", err)
}
t.Logf("Extracted %d command references from skill docs", len(refs))
type failure struct {
ref cmdRef
reason string
}
var fails []failure
seen := map[string]bool{} // dedupe identical sub-paths
for _, r := range refs {
sub, _, _ := parseSubPath(r.Cmd)
if sub == "" {
continue
}
if antiPatternAllowlist[sub] {
continue
}
if seen[sub+"|"+r.File] {
continue
}
seen[sub+"|"+r.File] = true
// Compute parent + leaf
tokens := strings.Fields(sub)
if len(tokens) < 2 {
continue
}
parent := strings.Join(tokens[:len(tokens)-1], " ")
leaf := tokens[len(tokens)-1]
// Strategy: directly check sub's own help; if its Usage line starts
// with the sub-path, the sub-command is registered. Otherwise cobra
// fell back to a parent and the sub doesn't exist.
subHelp := cache.get(sub)
expectedUsage := "Usage:\n " + sub
if strings.Contains(subHelp, expectedUsage) {
continue // sub-path exists, all good
}
// Sub may be a leaf with subcommands list — also valid (e.g. dws sheet)
// In that case Usage line is "Usage:\n <sub> [flags]\n <sub> [command]"
if strings.Contains(subHelp, "Usage:\n "+sub+" [flags]\n "+sub+" [command]") {
continue
}
// Otherwise verify by looking at parent's Available Commands
parentHelp := cache.get(parent)
available := parseAvailable(parentHelp)
found := false
for _, c := range available {
if c == leaf {
found = true
break
}
}
if !found {
fails = append(fails, failure{ref: r, reason: fmt.Sprintf("`%s` not in Available Commands of `%s`: %v", leaf, parent, available)})
}
}
if len(fails) == 0 {
t.Logf("✓ All extracted commands dispatch cleanly (sub-paths registered, anti-patterns whitelisted)")
return
}
// Format failure report
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Found %d skill-doc commands that don't dispatch in open-source dws v1.0.30:\n\n", len(fails)))
for _, f := range fails {
rel, _ := filepath.Rel(root, f.ref.File)
sb.WriteString(fmt.Sprintf(" ❌ %s:%d\n cmd: %s\n reason: %s\n\n", rel, f.ref.Line, f.ref.Cmd, f.reason))
}
t.Fatal(sb.String())
}