package core import ( "encoding/json" "strings" "testing" ) func validSpec() SurfaceSpec { return SurfaceSpec{ Title: "三家候选工具选型对比", Nodes: []UINode{ {Type: UISection, ID: "n1", Title: "一、概览", Children: []UINode{ {Type: UIMarkdown, ID: "n1a", Source: []string{"doc:eval/rc-01/vendor1"}, Markdown: "本期评估三家候选。"}, }}, {Type: UITable, ID: "n2", Source: []string{"doc:eval/rc-01/vendor1", "doc:eval/rc-01/vendor2"}, Table: &UITableSpec{ Columns: []UITableColumn{{Key: "k", Label: "维度"}, {Key: "v", Label: "值"}}, Rows: []UITableRow{{ID: "r1", Cells: [][2]string{{"k", "价格"}, {"v", "¥8/人/天"}}}}, }}, {Type: UISteps, ID: "n3", Steps: &UIStepsSpec{Current: 1, Steps: []UIStep{ {Label: "需求冻结", Status: "done"}, {Label: "契约定义", Status: "active"}, }}}, {Type: UIForm, ID: "n4", Fields: []UIField{ {Key: "years", Label: "归档年限", Type: "select", Options: []string{"3", "5"}, Required: true}, }}, }, } } func TestValidateSurfaceSpecValid(t *testing.T) { if errs := ValidateSurfaceSpec(validSpec(), nil); len(errs) != 0 { t.Fatalf("合法规格不应有违规: %v", errs) } // JSON 往返后仍合法(信封序列化面)。 b, err := json.Marshal(validSpec()) if err != nil { t.Fatal(err) } var back SurfaceSpec if err := json.Unmarshal(b, &back); err != nil { t.Fatal(err) } if errs := ValidateSurfaceSpec(back, nil); len(errs) != 0 { t.Fatalf("JSON 往返后不应有违规: %v", errs) } } func TestValidateSurfaceSpecViolations(t *testing.T) { cases := []struct { name string mut func(*SurfaceSpec) want string }{ {"未知类型", func(s *SurfaceSpec) { s.Nodes[0].Type = " hologram" }, "未知节点类型"}, {"重复节点 id", func(s *SurfaceSpec) { s.Nodes[1].ID = "n1" }, "重复节点 id"}, {"表格行列不齐", func(s *SurfaceSpec) { s.Nodes[1].Table.Rows = []UITableRow{{Cells: [][2]string{{"k", "只有一列"}}}} }, "不等长"}, {"select 缺 options", func(s *SurfaceSpec) { s.Nodes[3].Fields[0].Options = nil }, "缺 options"}, {"steps 越界", func(s *SurfaceSpec) { s.Nodes[2].Steps.Current = 5 }, "current 在范围内"}, {"空 title", func(s *SurfaceSpec) { s.Title = "" }, "title 必填"}, {"空 nodes", func(s *SurfaceSpec) { s.Nodes = nil }, "nodes 需非空"}, {"markdown 空", func(s *SurfaceSpec) { s.Nodes[0].Children[0].Markdown = "" }, "markdown 需要正文"}, {"progress 越界", func(s *SurfaceSpec) { s.Nodes = append(s.Nodes, UINode{Type: UIProgress, Progress: &UIProgressSpec{Value: 1.5}}) }, "progress.value"}, {"未知来源", func(s *SurfaceSpec) {}, ""}, } for _, c := range cases { s := validSpec() c.mut(&s) errs := ValidateSurfaceSpec(s, nil) if c.want == "" { continue } found := false for _, e := range errs { if strings.Contains(e, c.want) { found = true break } } if !found { t.Errorf("%s: 期望包含 %q,得到 %v", c.name, c.want, errs) } } // knownSources 非空时:未知来源拒绝、已知来源放行。 s := validSpec() errs := ValidateSurfaceSpec(s, map[string]bool{"doc:eval/rc-01/vendor1": true, "doc:eval/rc-01/vendor2": true}) if len(errs) != 0 { t.Fatalf("已知来源全引用不应违规: %v", errs) } s.Nodes[0].Children[0].Source = append(s.Nodes[0].Children[0].Source, "doc:eval/rc-99/ghost") errs = ValidateSurfaceSpec(s, map[string]bool{"doc:eval/rc-01/vendor1": true}) if len(errs) == 0 { t.Fatal("引用未知来源应拒绝") } } func TestValidateSurfaceSpecLimits(t *testing.T) { deep := UINode{Type: UISection, ID: "d0"} cur := &deep for i := 1; i <= SurfaceMaxDepth+1; i++ { cur.Children = []UINode{{Type: UISection, ID: "d" + string(rune('0'+i%10))}} cur = &cur.Children[0] } if errs := ValidateSurfaceSpec(SurfaceSpec{Title: "t", Nodes: []UINode{deep}}, nil); len(errs) == 0 { t.Fatal("超过嵌套深度应拒绝") } many := make([]UINode, SurfaceMaxNodes+1) for i := range many { many[i] = UINode{Type: UIMarkdown, Markdown: "x"} } if errs := ValidateSurfaceSpec(SurfaceSpec{Title: "t", Nodes: many}, nil); len(errs) == 0 { t.Fatal("超过节点数应拒绝") } } func TestUINodeBackwardCompat(t *testing.T) { // 旧卡片路径的 UINode JSON(无 id/source)解出后字段为零值,不受影响。 var n UINode if err := json.Unmarshal([]byte(`{"type":"list","items":["a"]}`), &n); err != nil { t.Fatal(err) } if n.ID != "" || n.Source != nil || n.Type != UIList || len(n.Items) != 1 { t.Fatalf("旧负载解引用异常: %+v", n) } // 新字段双向序列化。 b, _ := json.Marshal(UINode{Type: UIMarkdown, ID: "m1", Source: []string{"s1"}, Markdown: "x"}) if !strings.Contains(string(b), `"id":"m1"`) || !strings.Contains(string(b), `"source":["s1"]`) { t.Fatalf("新字段序列化缺失: %s", b) } }