// 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 }