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>
103 lines
3.5 KiB
Go
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")
|
|
}
|
|
}
|