Why: Allow consumers to download an immutable module revision without nested source checkouts or local path replacements. Evidence: SELF package tests and the prior canonical-path rejection are recorded in the Dev dependency-workspace task card; remote consumption is replayed against the committed candidate. Impact: Change the module identity and its Go imports while preserving API behavior. Existing historical submodule revisions remain buildable. Refs: Versioned dependencies U1/U3; feature/dependency-workspace
154 lines
5.1 KiB
Go
154 lines
5.1 KiB
Go
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":"主题是什么?"}]}`)
|
||
}
|