Files
Agent-System-Backend/internal/workflow/workflow_test.go
T
weichenandCursor 63ea1a262c Register plugin workflows and drop them on uninstall
Why: Intent routing needs a registry of workflow definitions, and uninstall was leaving intents and workflows in place.
Evidence: go test ./internal/workflow/ ./internal/plugins/ ./internal/app/ passed. Validation rejects duplicate ids, unknown kinds, denied tools, and both step caps. Unregister clears the registry and the intent gate.
Impact: Script steps must stay inside the plugin directory. The server holds the registry for later routing.
Refs: feature/agent-harness-p4
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-01 15:59:54 +00:00

103 lines
3.5 KiB
Go

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")
}
}