package workflow import ( "encoding/json" "strconv" "strings" "testing" core "agent-system-core" ) func known(names ...string) func(string) bool { set := map[string]struct{}{} for _, n := range names { set[n] = struct{}{} } return func(name string) bool { _, ok := set[name] return ok } } func TestValidateRejectsBadDefinitions(t *testing.T) { okTool := known("time_now") cases := []struct { name string def core.WorkflowDef lim Limits want string }{ {"dup", core.WorkflowDef{ID: "a", Steps: []core.WorkflowStep{{ID: "s", Kind: "hitl"}, {ID: "s", Kind: "card"}}}, DeclarativeLimits(), "重复"}, {"kind", core.WorkflowDef{ID: "a", Steps: []core.WorkflowStep{{ID: "s", Kind: "nope"}}}, DeclarativeLimits(), "不合法"}, {"denied", core.WorkflowDef{ID: "a", Steps: []core.WorkflowStep{{ID: "s", Kind: "tool", Tool: "run_python"}}}, DeclarativeLimits(), "不能调用"}, {"missing", core.WorkflowDef{ID: "a", Steps: []core.WorkflowStep{{ID: "s", Kind: "tool", Tool: "no_such"}}}, DeclarativeLimits(), "未注册"}, {"over", core.WorkflowDef{ID: "a", Steps: manySteps(21)}, DeclarativeLimits(), "超出上限"}, {"dynamic", core.WorkflowDef{ID: "a", Steps: manySteps(9)}, DynamicLimits(), "超出上限"}, {"script", core.WorkflowDef{ID: "a", Steps: []core.WorkflowStep{{ID: "s", Kind: "script", Script: "scripts/demo.py"}}}, DynamicLimits(), "不能包含 script"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { err := Validate(tc.def, tc.lim, okTool) if err == nil || !strings.Contains(err.Error(), tc.want) { t.Fatalf("got %v, want substring %q", err, tc.want) } }) } good := core.WorkflowDef{ID: "a", MaxRetries: 6, Steps: []core.WorkflowStep{ {ID: "s", Kind: "tool", Tool: "time_now"}, {ID: "p", Kind: "script", Script: "scripts/demo.py", Critic: &core.StepCritic{Criteria: "有结果", MaxRetries: 3}}, }} if err := Validate(good, DeclarativeLimits(), okTool); err != nil { t.Fatal(err) } } func manySteps(n int) []core.WorkflowStep { out := make([]core.WorkflowStep, n) for i := range out { out[i] = core.WorkflowStep{ID: "s" + strconv.Itoa(i), Kind: "hitl"} } return out } func TestRenderKeepsArgsJSON(t *testing.T) { raw := json.RawMessage(`{"q":"关于 {{slots.topic}}","note":"{{input}}"}`) got, err := RenderRaw(raw, Vars{Input: `他说 "好"`, Slots: map[string]string{"topic": "主题"}}) if err != nil { t.Fatal(err) } var m map[string]string if err := json.Unmarshal(got, &m); err != nil { t.Fatalf("not json: %s (%v)", got, err) } if m["q"] != "关于 主题" || m["note"] != `他说 "好"` { t.Fatalf("%s", got) } text := Render("{{steps.draft.output}} / {{slots.missing}}", Vars{Steps: map[string]string{"draft": "正文"}}) if text != "正文 / " { t.Fatalf("%q", text) } } func TestByIntentAndReplace(t *testing.T) { reg := NewRegistry() def := core.WorkflowDef{ID: "demo.brief", IntentID: "demo.brief", Steps: []core.WorkflowStep{{ID: "s", Kind: "hitl"}}} reg.Set([]core.WorkflowDef{def}, map[string]string{ScriptKey("demo.brief", "scripts/demo.py"): "print(1)"}) got, ok := reg.ByIntent("demo.brief") if !ok || got.ID != "demo.brief" { t.Fatalf("%+v %v", got, ok) } body, ok := reg.Script("demo.brief", "scripts/demo.py") if !ok || body != "print(1)" { t.Fatalf("script %q %v", body, ok) } reg.Set(nil, nil) if _, ok := reg.ByIntent("demo.brief"); ok { t.Fatal("cleared registry still has the workflow") } if _, ok := reg.Get("demo.brief"); ok { t.Fatal("cleared registry still has the id") } }