Why: A hit intent should start the bound workflow, and a command that shares the same id still has to win. Evidence: go test ./internal/intentgate/ ./internal/runmgr/ ./internal/server/ passed. Recognize keeps slots and the missing-slot question. StartWorkflowRun copies the definition and selects the workflow runner even when mock is on. Impact: Ordinary turns are unchanged. The workflow runner body is still a stub until the executor lands. Refs: feature/agent-harness-p4 Co-authored-by: Cursor <cursoragent@cursor.com>
112 lines
3.5 KiB
Go
112 lines
3.5 KiB
Go
package server
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"testing"
|
||
"time"
|
||
|
||
agentcommand "agent-command"
|
||
"agent-system-backend/internal/cmdbus"
|
||
"agent-system-backend/internal/intentgate"
|
||
"agent-system-backend/internal/runtime"
|
||
"agent-system-backend/internal/settings"
|
||
"agent-system-backend/internal/store"
|
||
"agent-system-backend/internal/workflow"
|
||
core "agent-system-core"
|
||
)
|
||
|
||
type routeCapture struct {
|
||
ch chan workflow.Launch
|
||
}
|
||
|
||
func (c routeCapture) Name() string { return "route-capture" }
|
||
|
||
func (c routeCapture) Execute(rc runtime.RunCtx) error {
|
||
src, _ := rc.(interface {
|
||
Workflow() (workflow.Launch, bool)
|
||
})
|
||
if src == nil {
|
||
return nil
|
||
}
|
||
if launch, ok := src.Workflow(); ok {
|
||
c.ch <- launch
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func TestCommandBusBeatsWorkflow(t *testing.T) {
|
||
srv, st := newTestServer(t)
|
||
if err := st.MergeSettingsScope(store.OwnerDefault, settings.ScopeUser, map[string]any{"runtime": map[string]any{"mock": false}}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
ch := make(chan workflow.Launch, 1)
|
||
srv.mgr.SetWorkflowRunner(routeCapture{ch})
|
||
gate := intentgate.New()
|
||
if err := gate.Rebuild(context.Background(), []json.RawMessage{briefIntent()}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
srv.Gate = gate
|
||
reg := workflow.NewRegistry()
|
||
reg.Set([]core.WorkflowDef{{
|
||
ID: "demo.brief", Title: "简报", IntentID: "demo.brief",
|
||
Steps: []core.WorkflowStep{{ID: "s", Kind: "hitl"}},
|
||
}}, nil)
|
||
srv.Workflows = reg
|
||
if err := srv.Bus.RegisterBuiltin(agentcommand.CommandSpec{ID: "demo.brief", Title: "简报命令"}, "", func(context.Context, cmdbus.Request) (any, error) {
|
||
return map[string]any{"ok": true}, nil
|
||
}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
_, convID := setupConversation(t, srv.Handler())
|
||
code, out := doJSON(t, srv.Handler(), "POST", "/api/conversations/"+convID+"/messages", map[string]any{"text": "写一份简报"})
|
||
if code != 200 || out["command"] != "demo.brief" {
|
||
t.Fatalf("status %d body %v", code, out)
|
||
}
|
||
if _, ok := out["run"]; ok {
|
||
t.Fatal("a command hit must not start a workflow run")
|
||
}
|
||
select {
|
||
case <-ch:
|
||
t.Fatal("workflow runner started for a command")
|
||
default:
|
||
}
|
||
}
|
||
|
||
func TestIntentStartsWorkflowRun(t *testing.T) {
|
||
srv, st := newTestServer(t)
|
||
if err := st.MergeSettingsScope(store.OwnerDefault, settings.ScopeUser, map[string]any{"runtime": map[string]any{"mock": false}}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
ch := make(chan workflow.Launch, 1)
|
||
srv.mgr.SetWorkflowRunner(routeCapture{ch})
|
||
gate := intentgate.New()
|
||
if err := gate.Rebuild(context.Background(), []json.RawMessage{briefIntent()}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
srv.Gate = gate
|
||
reg := workflow.NewRegistry()
|
||
reg.Set([]core.WorkflowDef{{
|
||
ID: "demo.brief", Title: "简报", IntentID: "demo.brief",
|
||
Steps: []core.WorkflowStep{{ID: "s", Kind: "hitl"}},
|
||
}}, nil)
|
||
srv.Workflows = reg
|
||
_, convID := setupConversation(t, srv.Handler())
|
||
code, out := doJSON(t, srv.Handler(), "POST", "/api/conversations/"+convID+"/messages", map[string]any{"text": "写一份简报"})
|
||
if code != 200 || out["run"] == nil {
|
||
t.Fatalf("status %d body %v", code, out)
|
||
}
|
||
select {
|
||
case launch := <-ch:
|
||
if launch.Def.ID != "demo.brief" || len(launch.Missing) != 1 || launch.Missing[0].Question != "主题是什么?" {
|
||
t.Fatalf("%+v", launch)
|
||
}
|
||
case <-time.After(time.Second):
|
||
t.Fatal("workflow runner was not selected")
|
||
}
|
||
}
|
||
|
||
func briefIntent() json.RawMessage {
|
||
return json.RawMessage(`{"id":"demo.brief","domain":"demo","version":1,"matcher":{"includes_any":["简报"]},"slots":[{"key":"topic","required":true,"pattern":"主题[::]\\s*(\\S+)","question":"主题是什么?"}]}`)
|
||
}
|