Files
dingtalk-workspace-cli/test/unit/whiteboard_skill_docs_test.go

128 lines
3.7 KiB
Go

// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package unit_test
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
)
func TestWhiteboardQuickExamplesUseWritableTextNodes(t *testing.T) {
paths := []string{
"../../skills/mono/references/products/whiteboard.md",
"../../skills/multi/dingtalk-misc/references/whiteboard.md",
}
for _, path := range paths {
path := path
t.Run(filepath.Base(filepath.Dir(path))+"/"+filepath.Base(path), func(t *testing.T) {
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
payload := firstJSONFence(t, string(data))
source, _ := payload["source"].(map[string]any)
nodes, _ := source["nodes"].([]any)
if len(nodes) == 0 {
t.Fatalf("quick example has no source.nodes: %#v", payload)
}
node, _ := nodes[0].(map[string]any)
if node["type"] != "text" {
t.Fatalf("quick example first node type = %#v, want text", node["type"])
}
for _, dimension := range []string{"width", "height"} {
value, ok := node[dimension].(float64)
if !ok || value <= 0 {
t.Errorf("quick example %s = %#v, want positive number", dimension, node[dimension])
}
}
text, ok := node["text"].(map[string]any)
if !ok {
t.Fatalf("quick example text = %#v, want OpenNodes text object", node["text"])
}
blocks, _ := text["blocks"].([]any)
if len(blocks) == 0 {
t.Fatalf("quick example text.blocks = %#v, want non-empty array", text["blocks"])
}
})
}
}
func TestWhiteboardRecipesAreDeliveredToBothSkillSurfaces(t *testing.T) {
monoPath := "../../skills/mono/references/products/whiteboard/recipes.md"
multiPath := "../../skills/multi/dingtalk-misc/references/whiteboard/recipes.md"
mono, err := os.ReadFile(monoPath)
if err != nil {
t.Fatal(err)
}
multi, err := os.ReadFile(multiPath)
if err != nil {
t.Fatal(err)
}
if string(mono) != string(multi) {
t.Fatal("mono and multi whiteboard recipes differ")
}
content := string(mono)
for _, required := range []string{
"## 1. 追加两个流程节点和一条箭头",
"## 2. 追加带渐变和阴影的卡片",
"## 3. Frame 中放置分支流程",
"## 4. 上传 SVG 并追加 Vector",
"## 5. 整页替换或清空",
"执行任何远端写入前,必须先向用户展示影响并取得明确确认",
`"resourceUrl": "https://resource.example/resource-stable-id?resourceId=resource-stable-id"`,
`"url": "https://resource.example/resource-stable-id?resourceId=resource-stable-id"`,
} {
if !strings.Contains(content, required) {
t.Errorf("whiteboard recipes missing %q", required)
}
}
for _, fence := range bashFences(content) {
if strings.Contains(fence, "dws whiteboard update") || strings.Contains(fence, "dws doc media upload") {
if !strings.Contains(fence, "--yes") {
t.Errorf("whiteboard write example lacks --yes:\n%s", fence)
}
}
}
}
func bashFences(markdown string) []string {
const marker = "```bash\n"
var fences []string
for {
start := strings.Index(markdown, marker)
if start < 0 {
return fences
}
markdown = markdown[start+len(marker):]
end := strings.Index(markdown, "\n```")
if end < 0 {
return fences
}
fences = append(fences, markdown[:end])
markdown = markdown[end+len("\n```"):]
}
}
func firstJSONFence(t *testing.T, markdown string) map[string]any {
t.Helper()
const marker = "```json\n"
start := strings.Index(markdown, marker)
if start < 0 {
t.Fatal("markdown has no JSON fence")
}
start += len(marker)
end := strings.Index(markdown[start:], "\n```")
if end < 0 {
t.Fatal("JSON fence is not closed")
}
var payload map[string]any
if err := json.Unmarshal([]byte(markdown[start:start+end]), &payload); err != nil {
t.Fatalf("decode first JSON fence: %v", err)
}
return payload
}