package server import ( "context" "encoding/json" "testing" "time" agentcommand "git-ssh.u1680832.nyat.app/weichen/Agent-Command.git" "git-ssh.u1680832.nyat.app/weichen/Agent-System-Backend.git/internal/cmdbus" "git-ssh.u1680832.nyat.app/weichen/Agent-System-Backend.git/internal/intentgate" "git-ssh.u1680832.nyat.app/weichen/Agent-System-Backend.git/internal/runtime" "git-ssh.u1680832.nyat.app/weichen/Agent-System-Backend.git/internal/settings" "git-ssh.u1680832.nyat.app/weichen/Agent-System-Backend.git/internal/store" "git-ssh.u1680832.nyat.app/weichen/Agent-System-Backend.git/internal/workflow" core "git-ssh.u1680832.nyat.app/weichen/Agent-System-Core.git" ) 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 TestWorkflowRouteClaimsBeforePersist(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) } 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()) conv, err := st.GetConversation(store.OwnerDefault, convID) if err != nil { t.Fatal(err) } if err := srv.mgr.ClaimConversation(convID); err != nil { t.Fatal(err) } before, err := st.ListMessages(convID) if err != nil { t.Fatal(err) } code, out := doJSON(t, srv.Handler(), "POST", "/api/conversations/"+convID+"/messages", map[string]any{"text": "写一份简报"}) if code != 409 { t.Fatalf("status %d body %v", code, out) } after, err := st.ListMessages(convID) if err != nil { t.Fatal(err) } if len(after) != len(before) { t.Fatalf("workflow route wrote a message while busy") } srv.mgr.ReleaseConversation(conv.ID) } 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":"主题是什么?"}]}`) }