package dsl import ( "encoding/json" "strings" "testing" ) func TestParseAndAssemble_OK(t *testing.T) { raw := json.RawMessage(`{"version":"1","nodes":[{"id":"a","kind":"input"}],"edges":[]}`) task, err := ParseAndAssemble(raw) if err != nil { t.Fatalf("合法 DSL 不应报错: %v", err) } if !strings.HasPrefix(task.ID, "task_") { t.Errorf("任务 ID 应以 task_ 前缀, got %q", task.ID) } if string(task.Graph) != string(raw) { t.Error("Graph 应原样透传 DSL") } if task.Meta == nil { t.Error("Meta 应已初始化(供网关注入身份)") } } func TestParseAndAssemble_Errors(t *testing.T) { if _, err := ParseAndAssemble(json.RawMessage(``)); err == nil { t.Error("空 DSL 应报错") } if _, err := ParseAndAssemble(json.RawMessage(`{bad`)); err == nil { t.Error("非法 JSON 应报错") } } func TestParseAndAssemble_Topology(t *testing.T) { ok := func(raw string) { t.Helper() if _, err := ParseAndAssemble(json.RawMessage(raw)); err != nil { t.Errorf("应通过: %s\n err=%v", raw, err) } } bad := func(raw, wantSub string) { t.Helper() _, err := ParseAndAssemble(json.RawMessage(raw)) if err == nil { t.Errorf("应被拒: %s", raw) return } if !strings.Contains(err.Error(), wantSub) { t.Errorf("错误应含 %q, got %q", wantSub, err.Error()) } } // 合法:多节点 + 边引用齐全。 ok(`{"nodes":[{"id":"a","kind":"input"},{"id":"b","kind":"agent"}],"edges":[{"source":"a","target":"b"}]}`) // 宽松放过:空图 / 报告任务的 {topic} / 无 nodes 字段。 ok(`{"nodes":[],"edges":[]}`) ok(`{"topic":"季度报告"}`) ok(`{"version":"1"}`) // 拒绝:重复 id。 bad(`{"nodes":[{"id":"a"},{"id":"a"}],"edges":[]}`, "重复") // 拒绝:空 id。 bad(`{"nodes":[{"id":""}],"edges":[]}`, "缺少 id") // 拒绝:悬挂边(目标节点不存在)。 bad(`{"nodes":[{"id":"a"}],"edges":[{"source":"a","target":"ghost"}]}`, "不存在的目标节点") // 拒绝:悬挂边(源节点不存在,即便无节点)。 bad(`{"nodes":[],"edges":[{"source":"x","target":"y"}]}`, "不存在的源节点") // 拒绝:边端点为空。 bad(`{"nodes":[{"id":"a"}],"edges":[{"source":"a","target":""}]}`, "不能为空") }