581 lines
17 KiB
Go
581 lines
17 KiB
Go
package unit_test
|
|
|
|
import (
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestSkillDocsDoNotRecommendRetiredCommands(t *testing.T) {
|
|
_, filename, _, ok := runtime.Caller(0)
|
|
if !ok {
|
|
t.Fatal("runtime.Caller(0) failed")
|
|
}
|
|
|
|
root := filepath.Clean(filepath.Join(filepath.Dir(filename), "..", ".."))
|
|
skillsDir := filepath.Join(root, "skills")
|
|
retiredCommands := []string{
|
|
"chat file upload",
|
|
"conference start",
|
|
"conference get-id",
|
|
"conference member invite",
|
|
"conference share",
|
|
"dingtalk-conference",
|
|
}
|
|
allowedContext := []string{
|
|
"已下线",
|
|
"下线",
|
|
"不支持",
|
|
"不要",
|
|
"无需",
|
|
"当前 CLI 不支持",
|
|
"兼容提示",
|
|
"不可用",
|
|
"钉钉客户端",
|
|
}
|
|
|
|
var violations []string
|
|
err := filepath.WalkDir(skillsDir, func(path string, d fs.DirEntry, walkErr error) error {
|
|
if walkErr != nil {
|
|
return walkErr
|
|
}
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
if filepath.Ext(path) != ".md" {
|
|
return nil
|
|
}
|
|
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return fmt.Errorf("read %s: %w", path, err)
|
|
}
|
|
rel, _ := filepath.Rel(root, path)
|
|
for i, line := range strings.Split(string(content), "\n") {
|
|
for _, retired := range retiredCommands {
|
|
if !strings.Contains(line, retired) {
|
|
continue
|
|
}
|
|
if hasAny(line, allowedContext) {
|
|
continue
|
|
}
|
|
violations = append(violations, fmt.Sprintf("%s:%d recommends retired command %q: %s", rel, i+1, retired, strings.TrimSpace(line)))
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("WalkDir() error = %v", err)
|
|
}
|
|
if len(violations) > 0 {
|
|
t.Fatalf("skill docs recommend retired commands:\n%s", strings.Join(violations, "\n"))
|
|
}
|
|
}
|
|
|
|
func TestEventSkillUsesFlatOutputContract(t *testing.T) {
|
|
_, filename, _, ok := runtime.Caller(0)
|
|
if !ok {
|
|
t.Fatal("runtime.Caller(0) failed")
|
|
}
|
|
root := filepath.Clean(filepath.Join(filepath.Dir(filename), "..", ".."))
|
|
paths := []string{
|
|
filepath.Join(root, "skills", "multi", "dingtalk-event", "SKILL.md"),
|
|
filepath.Join(root, "skills", "multi", "dingtalk-event", "references", "event-im.md"),
|
|
filepath.Join(root, "skills", "mono", "references", "products", "event.md"),
|
|
}
|
|
for _, path := range paths {
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read %s: %v", path, err)
|
|
}
|
|
text := string(content)
|
|
for _, required := range []string{
|
|
"[event] ready",
|
|
"--flatten",
|
|
"conversation_id",
|
|
"sender_open_dingtalk_id",
|
|
"reader_open_dingtalk_id",
|
|
"recaller_open_dingtalk_id",
|
|
"reaction_name",
|
|
"operation_type",
|
|
"dws chat message download-media",
|
|
"--open-dingtalk-id",
|
|
"user_im_message_receive_o2o_all",
|
|
"user_im_message_receive_group_all",
|
|
"user_im_group_updated",
|
|
"user_im_group_member_added",
|
|
"user_im_group_member_exited",
|
|
"user_im_group_disbanded",
|
|
"operator_open_dingtalk_id",
|
|
"members",
|
|
"open_dingtalk_id",
|
|
} {
|
|
if !strings.Contains(text, required) {
|
|
t.Errorf("%s missing event contract %q", path, required)
|
|
}
|
|
}
|
|
for _, retired := range []string{
|
|
"payload.body.",
|
|
"尚无稳定业务样本",
|
|
"暂无稳定 payload schema",
|
|
} {
|
|
if strings.Contains(text, retired) {
|
|
t.Errorf("%s still documents retired event path %q", path, retired)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageEventSkillPinsSubscriptionRetryOrchestrationContract(t *testing.T) {
|
|
_, filename, _, ok := runtime.Caller(0)
|
|
if !ok {
|
|
t.Fatal("runtime.Caller(0) failed")
|
|
}
|
|
root := filepath.Clean(filepath.Join(filepath.Dir(filename), "..", ".."))
|
|
paths := []string{
|
|
filepath.Join(root, "skills", "multi", "dingtalk-event", "SKILL.md"),
|
|
filepath.Join(root, "skills", "multi", "dingtalk-event", "references", "event-im.md"),
|
|
filepath.Join(root, "skills", "mono", "references", "products", "event.md"),
|
|
filepath.Join(root, "docs", "event-subprocess-contract.md"),
|
|
}
|
|
for _, path := range paths {
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read %s: %v", path, err)
|
|
}
|
|
text := string(content)
|
|
normalizedText := strings.Join(strings.Fields(text), " ")
|
|
for _, required := range []string{
|
|
"16",
|
|
"--profile",
|
|
"Agent/host",
|
|
"0/2/1",
|
|
"retryable=false",
|
|
"max_additional_attempts=0",
|
|
"retryable=true",
|
|
"max_additional_attempts=2",
|
|
"retryable=unknown",
|
|
"max_additional_attempts=1",
|
|
"retry_after_seconds",
|
|
"next_retry_at",
|
|
"in_flight",
|
|
"cooldown",
|
|
"terminal_hold",
|
|
"subscribe_id",
|
|
"trace_id",
|
|
} {
|
|
if !strings.Contains(text, required) {
|
|
t.Errorf("%s missing subscription retry orchestration contract %q", path, required)
|
|
}
|
|
}
|
|
if path == filepath.Join(root, "docs", "event-subprocess-contract.md") {
|
|
for _, required := range []string{
|
|
"not a CLI-enforced persisted total-attempt cap",
|
|
"performs no in-process automatic retry",
|
|
"does not persist or enforce the Agent/host attempt count",
|
|
} {
|
|
if !strings.Contains(normalizedText, required) {
|
|
t.Errorf("%s overstates CLI retry enforcement; missing %q", path, required)
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
for _, required := range []string{
|
|
"不是 CLI 持久化硬总次数上限",
|
|
"进程内不会自动重试",
|
|
"不持久化或计算跨调用的 Agent/host 尝试次数",
|
|
} {
|
|
if !strings.Contains(normalizedText, required) {
|
|
t.Errorf("%s overstates CLI retry enforcement; missing %q", path, required)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageEventSkillDocumentsSubscriptionGuardOperations(t *testing.T) {
|
|
_, filename, _, ok := runtime.Caller(0)
|
|
if !ok {
|
|
t.Fatal("runtime.Caller(0) failed")
|
|
}
|
|
root := filepath.Clean(filepath.Join(filepath.Dir(filename), "..", ".."))
|
|
paths := []string{
|
|
filepath.Join(root, "skills", "multi", "dingtalk-event", "SKILL.md"),
|
|
filepath.Join(root, "skills", "multi", "dingtalk-event", "references", "event-im.md"),
|
|
filepath.Join(root, "skills", "mono", "references", "products", "event.md"),
|
|
filepath.Join(root, "docs", "event-subprocess-contract.md"),
|
|
}
|
|
for _, path := range paths {
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read %s: %v", path, err)
|
|
}
|
|
text := string(content)
|
|
normalizedText := strings.Join(strings.Fields(text), " ")
|
|
for _, required := range []string{
|
|
"~/.dws/events/open/personal_stream/<identity_hash>/personal_subscription_attempts.json",
|
|
"DWS_CONFIG_DIR",
|
|
"personal_subscription_attempts.json",
|
|
"personal_subscription_attempts.lock",
|
|
"0700",
|
|
"0600",
|
|
"24h",
|
|
"1h",
|
|
"terminal_hold",
|
|
"next_retry_at",
|
|
} {
|
|
if !strings.Contains(text, required) {
|
|
t.Errorf("%s missing subscription guard operations %q", path, required)
|
|
}
|
|
}
|
|
if path == filepath.Join(root, "docs", "event-subprocess-contract.md") {
|
|
for _, required := range []string{
|
|
"Delete only",
|
|
"never the lock file",
|
|
"every protection record for that identity",
|
|
} {
|
|
if !strings.Contains(normalizedText, required) {
|
|
t.Errorf("%s missing emergency guard-clear warning %q", path, required)
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
for _, required := range []string{
|
|
"只删除 `personal_subscription_attempts.json`",
|
|
"不要删除 lock 文件",
|
|
"该 identity 的全部保护记录",
|
|
} {
|
|
if !strings.Contains(normalizedText, required) {
|
|
t.Errorf("%s missing emergency guard-clear warning %q", path, required)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEventSkillFrontmatterAdvertisesGroupMemberLifecycle(t *testing.T) {
|
|
_, filename, _, ok := runtime.Caller(0)
|
|
if !ok {
|
|
t.Fatal("runtime.Caller(0) failed")
|
|
}
|
|
root := filepath.Clean(filepath.Join(filepath.Dir(filename), "..", ".."))
|
|
paths := []string{
|
|
filepath.Join(root, "skills", "mono", "SKILL.md"),
|
|
filepath.Join(root, "skills", "multi", "dingtalk-event", "SKILL.md"),
|
|
}
|
|
for _, path := range paths {
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read %s: %v", path, err)
|
|
}
|
|
parts := strings.SplitN(string(content), "---", 3)
|
|
if len(parts) != 3 {
|
|
t.Fatalf("%s missing YAML frontmatter", path)
|
|
}
|
|
frontmatter := parts[1]
|
|
for _, required := range []string{
|
|
"个人 IM",
|
|
"群成员加入",
|
|
"群成员退出",
|
|
"审批任务创建/完成/转交",
|
|
"审批实例发起/终止/完成",
|
|
} {
|
|
if !strings.Contains(frontmatter, required) {
|
|
t.Errorf("%s frontmatter missing event discovery trigger %q", path, required)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStandaloneEventSkillOwnsAllPersonalEventContracts(t *testing.T) {
|
|
_, filename, _, ok := runtime.Caller(0)
|
|
if !ok {
|
|
t.Fatal("runtime.Caller(0) failed")
|
|
}
|
|
root := filepath.Clean(filepath.Join(filepath.Dir(filename), "..", ".."))
|
|
eventRoot := filepath.Join(root, "skills", "multi", "dingtalk-event")
|
|
skillPath := filepath.Join(eventRoot, "SKILL.md")
|
|
skillContent, err := os.ReadFile(skillPath)
|
|
if err != nil {
|
|
t.Fatalf("read %s: %v", skillPath, err)
|
|
}
|
|
for _, required := range []string{
|
|
"../dingtalk-shared/SKILL.md",
|
|
"<!-- dws-intent: event.listen.im -->",
|
|
"<!-- dws-intent: event.listen.oa -->",
|
|
"16 个 EventKey",
|
|
"22 个公开个人 EventKey",
|
|
} {
|
|
if !strings.Contains(string(skillContent), required) {
|
|
t.Errorf("%s missing standalone event contract %q", skillPath, required)
|
|
}
|
|
}
|
|
|
|
wantReferences := []string{
|
|
"event-im-keys.md",
|
|
"event-im-lifecycle.md",
|
|
"event-im-operations.md",
|
|
"event-im-output.md",
|
|
"event-im.md",
|
|
"event-oa.md",
|
|
}
|
|
var combined strings.Builder
|
|
combined.Write(skillContent)
|
|
for _, name := range wantReferences {
|
|
path := filepath.Join(eventRoot, "references", name)
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read %s: %v", path, err)
|
|
}
|
|
combined.Write(content)
|
|
}
|
|
refs, err := filepath.Glob(filepath.Join(eventRoot, "references", "event-*.md"))
|
|
if err != nil {
|
|
t.Fatalf("glob event references: %v", err)
|
|
}
|
|
if len(refs) != len(wantReferences) {
|
|
t.Errorf("event reference count = %d, want %d: %v", len(refs), len(wantReferences), refs)
|
|
}
|
|
|
|
allEventKeys := []string{
|
|
"user_im_message_receive_at",
|
|
"user_im_message_receive_o2o",
|
|
"user_im_message_receive_group",
|
|
"user_im_message_receive_user",
|
|
"user_im_message_receive_o2o_all",
|
|
"user_im_message_receive_group_all",
|
|
"user_im_message_read_o2o",
|
|
"user_im_message_read_group",
|
|
"user_im_message_recall_o2o",
|
|
"user_im_message_recall_group",
|
|
"user_im_message_reaction_o2o",
|
|
"user_im_message_reaction_group",
|
|
"user_im_group_updated",
|
|
"user_im_group_member_added",
|
|
"user_im_group_member_exited",
|
|
"user_im_group_disbanded",
|
|
"user_oa_approval_task_created",
|
|
"user_oa_approval_task_finished",
|
|
"user_oa_approval_task_redirected",
|
|
"user_oa_approval_instance_started",
|
|
"user_oa_approval_instance_terminated",
|
|
"user_oa_approval_instance_finished",
|
|
}
|
|
for _, eventKey := range allEventKeys {
|
|
if !strings.Contains(combined.String(), eventKey) {
|
|
t.Errorf("standalone event skill missing EventKey %q", eventKey)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMiscSkillDoesNotOwnPersonalEvent(t *testing.T) {
|
|
_, filename, _, ok := runtime.Caller(0)
|
|
if !ok {
|
|
t.Fatal("runtime.Caller(0) failed")
|
|
}
|
|
root := filepath.Clean(filepath.Join(filepath.Dir(filename), "..", ".."))
|
|
miscRoot := filepath.Join(root, "skills", "multi", "dingtalk-misc")
|
|
miscPath := filepath.Join(miscRoot, "SKILL.md")
|
|
content, err := os.ReadFile(miscPath)
|
|
if err != nil {
|
|
t.Fatalf("read %s: %v", miscPath, err)
|
|
}
|
|
parts := strings.SplitN(string(content), "---", 3)
|
|
if len(parts) != 3 {
|
|
t.Fatalf("%s missing YAML frontmatter", miscPath)
|
|
}
|
|
for _, forbidden := range []string{
|
|
"个人 IM 事件",
|
|
"实时监听消息",
|
|
"被@消息",
|
|
"群成员加入",
|
|
"事件驱动 Agent",
|
|
"dws event",
|
|
} {
|
|
if strings.Contains(parts[1], forbidden) {
|
|
t.Errorf("%s frontmatter still claims personal Event trigger %q", miscPath, forbidden)
|
|
}
|
|
}
|
|
for _, required := range []string{
|
|
"dingtalk-event",
|
|
"oa.md",
|
|
"dev/event.md",
|
|
} {
|
|
if !strings.Contains(string(content), required) {
|
|
t.Errorf("%s missing Event ownership handoff %q", miscPath, required)
|
|
}
|
|
}
|
|
|
|
retiredNames := []string{
|
|
"event.md",
|
|
"event-im.md",
|
|
"event-im-keys.md",
|
|
"event-im-lifecycle.md",
|
|
"event-im-operations.md",
|
|
"event-im-output.md",
|
|
"event-oa.md",
|
|
}
|
|
for _, name := range retiredNames {
|
|
path := filepath.Join(miscRoot, "references", name)
|
|
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
|
t.Errorf("misc retains retired personal Event reference %s", path)
|
|
}
|
|
}
|
|
devEventPath := filepath.Join(miscRoot, "references", "dev", "event.md")
|
|
if _, err := os.Stat(devEventPath); err != nil {
|
|
t.Errorf("DevApp event reference must remain at %s: %v", devEventPath, err)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageDocSkillPinsExactVersionRoutesAndHelpBudget(t *testing.T) {
|
|
_, filename, _, ok := runtime.Caller(0)
|
|
if !ok {
|
|
t.Fatal("runtime.Caller(0) failed")
|
|
}
|
|
root := filepath.Clean(filepath.Join(filepath.Dir(filename), "..", ".."))
|
|
paths := []string{
|
|
filepath.Join(root, "skills", "multi", "dingtalk-doc", "SKILL.md"),
|
|
filepath.Join(root, "skills", "multi", "dingtalk-doc", "references", "doc.md"),
|
|
filepath.Join(root, "skills", "mono", "references", "products", "doc.md"),
|
|
}
|
|
for _, path := range paths {
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read %s: %v", path, err)
|
|
}
|
|
text := string(content)
|
|
for _, required := range []string{
|
|
"dws doc +version-save --node",
|
|
"dws doc +version-list --node",
|
|
"dws doc +version-revert --node",
|
|
} {
|
|
if !strings.Contains(text, required) {
|
|
t.Errorf("%s missing exact version route %q", path, required)
|
|
}
|
|
}
|
|
if strings.Contains(text, "dws doc +history-") {
|
|
t.Errorf("%s recommends a history compatibility route", path)
|
|
}
|
|
}
|
|
|
|
rootSkill, err := os.ReadFile(paths[0])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, required := range []string{"Help 不参与选路", "unknown flag", "只查一次 shortcut 清单", "禁止试探后缀"} {
|
|
if !strings.Contains(string(rootSkill), required) {
|
|
t.Errorf("Doc root Skill missing Help budget rule %q", required)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageDocSkillPinsTemplateSearchRequiredQuery(t *testing.T) {
|
|
_, filename, _, ok := runtime.Caller(0)
|
|
if !ok {
|
|
t.Fatal("runtime.Caller(0) failed")
|
|
}
|
|
path := filepath.Clean(filepath.Join(filepath.Dir(filename), "..", "..", "skills", "multi", "dingtalk-doc", "SKILL.md"))
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
text := string(content)
|
|
for _, route := range []string{
|
|
"dws doc +template-list [--source MY\\|PUBLIC]",
|
|
"dws doc +template-search --query <名称或关键词>",
|
|
"dws doc +create-from-template --template-id <唯一ID>",
|
|
} {
|
|
if !strings.Contains(text, route) {
|
|
t.Errorf("Doc Golden Route does not publish mutually exclusive template route %q", route)
|
|
}
|
|
}
|
|
if strings.Contains(text, "`dws doc +template-search` →") {
|
|
t.Fatal("Doc Golden Route still recommends template-search without --query")
|
|
}
|
|
for _, required := range []string{
|
|
"准备 Help 时,本轮仅查一次",
|
|
"--fields use_when,avoid_when,parameters,constraints,confirmation",
|
|
"禁用产品级/`--all`",
|
|
"Help 不参与选路",
|
|
"禁止靠失败探测门禁",
|
|
"已有或临时文件先暂存到 cwd",
|
|
} {
|
|
if !strings.Contains(text, required) {
|
|
t.Errorf("Doc root Skill missing bounded Schema/input protocol %q", required)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageDocSkillReferenceLoadingBudget(t *testing.T) {
|
|
_, filename, _, ok := runtime.Caller(0)
|
|
if !ok {
|
|
t.Fatal("runtime.Caller(0) failed")
|
|
}
|
|
root := filepath.Clean(filepath.Join(filepath.Dir(filename), "..", ".."))
|
|
paths := []string{
|
|
filepath.Join(root, "skills", "multi", "dingtalk-doc", "SKILL.md"),
|
|
filepath.Join(root, "skills", "mono", "references", "products", "doc.md"),
|
|
}
|
|
for _, path := range paths {
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read %s: %v", path, err)
|
|
}
|
|
text := string(content)
|
|
for _, required := range []string{
|
|
"禁止读取 reference",
|
|
"最多读取一个",
|
|
"复杂 JSONML",
|
|
"append/overwrite",
|
|
} {
|
|
if !strings.Contains(text, required) {
|
|
t.Errorf("%s missing bounded Reference rule %q", path, required)
|
|
}
|
|
}
|
|
if strings.Contains(text, "只在任务命中时读取一个精确 reference") {
|
|
t.Errorf("%s restores task-match-driven Reference loading", path)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMinutesPermissionAddRequiresExplicitPolicy(t *testing.T) {
|
|
_, filename, _, ok := runtime.Caller(0)
|
|
if !ok {
|
|
t.Fatal("runtime.Caller(0) failed")
|
|
}
|
|
root := filepath.Clean(filepath.Join(filepath.Dir(filename), "..", ".."))
|
|
paths := []string{
|
|
filepath.Join(root, "skills", "mono", "references", "products", "minutes.md"),
|
|
filepath.Join(root, "skills", "multi", "dingtalk-minutes", "references", "minutes.md"),
|
|
}
|
|
for _, path := range paths {
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read %s: %v", path, err)
|
|
}
|
|
text := string(content)
|
|
for _, required := range []string{
|
|
"`permission add` 的 `--policy` 是必填参数,没有默认值",
|
|
"命令中仍必须显式传入 `--policy 4`",
|
|
} {
|
|
if !strings.Contains(text, required) {
|
|
t.Errorf("%s missing permission add policy contract %q", path, required)
|
|
}
|
|
}
|
|
if strings.Contains(text, "`permission add` 默认使用 `--policy 4`") {
|
|
t.Errorf("%s still documents a nonexistent permission add policy default", path)
|
|
}
|
|
}
|
|
}
|
|
|
|
func hasAny(s string, needles []string) bool {
|
|
for _, needle := range needles {
|
|
if strings.Contains(s, needle) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|