Add a mock workflow script and load the example plugin

Why: Mock mode needs a path that drives the real step runner, and the example plugin should fail the host test if its workflow or slot question is wrong.
Evidence: go test ./internal/plugins/ -run TestRepoExamplePluginLoads passed. The brief intent asks 主题是什么? and keeps the topic slot.
Impact: #workflow is only the mock runner. The example plugin itself is in the Dev repo.
Refs: feature/agent-harness-p4
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-01 16:21:50 +00:00
co-authored by Cursor
parent fb5f071761
commit 10186ab916
2 changed files with 74 additions and 1 deletions
+44
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"agent-system-backend/internal/intentgate"
@@ -208,3 +209,46 @@ func TestWorkflowScriptCannotEscape(t *testing.T) {
t.Fatal("script path outside the plugin dir should be rejected")
}
}
func TestRepoExamplePluginLoads(t *testing.T) {
root := filepath.Join("..", "..", "..", "plugins")
if _, err := os.Stat(filepath.Join(root, "example", "workflows", "demo-brief.json")); err != nil {
t.Skip("example plugin is not beside this module")
}
dir := t.TempDir()
st, err := store.OpenSQLite(filepath.Join(dir, "test.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { st.Close() })
regTools := tools.NewRegistry()
regTools.RegisterBuiltin()
host := New(st, regTools, settings.NewService(st), root)
gate := intentgate.New()
wf := workflow.NewRegistry()
host.OnIntents = func(overlay []json.RawMessage) error {
return gate.Rebuild(context.Background(), overlay)
}
host.OnWorkflows = func(defs []core.WorkflowDef, scripts map[string]string) error {
wf.Set(defs, scripts)
return nil
}
if err := host.LoadDir()["example"]; err != nil {
t.Fatal(err)
}
if _, ok := wf.ByIntent("demo.brief"); !ok {
t.Fatal("demo.brief is not bound")
}
body, ok := wf.Script("demo.script", "workflows/scripts/demo.py")
if !ok || !strings.Contains(body, `wf.tool("time_now"`) {
t.Fatalf("script %q", body)
}
got := gate.Recognize(context.Background(), "写一份简报")
if got.ID != "demo.brief" || len(got.Missing) != 1 || got.Missing[0].Question != "主题是什么?" {
t.Fatalf("%+v", got)
}
filled := gate.Recognize(context.Background(), "主题:核能 写一份简报")
if filled.Slots["topic"] != "核能" || len(filled.Missing) != 0 {
t.Fatalf("%+v", filled)
}
}
+30 -1
View File
@@ -7,6 +7,7 @@ import (
"agent-system-backend/internal/docgen"
"agent-system-backend/internal/squadadapter"
"agent-system-backend/internal/workflow"
core "agent-system-core"
)
@@ -212,6 +213,17 @@ func (m *MockRunner) Execute(rc RunCtx) error {
addText("Agent-Squad 编排完成:" + summary + "。步骤与子代理见右栏「计划」「代理」。")
}
case strings.Contains(user, "#workflow"):
res, err := (WorkflowRunner{}).run(rc, workflow.Launch{Def: mockWorkflow()})
if err != nil {
if res.Summary != "" {
addText(res.Summary)
}
addText("演示工作流没有跑完:" + err.Error())
break
}
parts = append(parts, res.Parts...)
case strings.Contains(user, "#error"):
return fmt.Errorf("mock: 演示失败路径(#error 指令)")
@@ -229,7 +241,7 @@ func (m *MockRunner) Execute(rc RunCtx) error {
core.Part{Type: core.PartToolResult, Process: true, ToolResult: &core.ToolResultPart{CallID: callID, Status: res.Status, Summary: truncateForJSON(res.Output, 120)}},
)
rc.Emit(core.EvRunPhase, core.RunPhasePayload{Phase: core.PhaseResponding})
reply := fmt.Sprintf("(mock 回复)你说:%s\n\n这是 mock runner 的脚本化回复——未调用真实模型。可发送 #hitl / #agents / #doc / #error 触发对应演示路径。", user)
reply := fmt.Sprintf("(mock 回复)你说:%s\n\n这是 mock runner 的脚本化回复——未调用真实模型。可发送 #hitl / #agents / #doc / #workflow / #error 触发对应演示路径。", user)
idx := 0
for _, r := range reply {
rc.Emit(core.EvMessageDelta, core.MessageDeltaPayload{Index: idx, Text: string(r)})
@@ -257,6 +269,23 @@ func (m *MockSubagent) Execute(rc RunCtx) error {
return nil
}
func mockWorkflow() core.WorkflowDef {
return core.WorkflowDef{
ID: "mock.workflow", Title: "演示工作流",
Steps: []core.WorkflowStep{
{ID: "length", Title: "篇幅", Kind: "hitl", Hitl: &core.HITLCardTemplate{
Question: "要多长?", Kind: core.HITLKindChoice, AllowFreeInput: false,
Choices: []core.HITLChoice{
{ID: "short", Label: "短", Description: "一段", Impact: "只写一段"},
{ID: "long", Label: "长", Description: "几段", Impact: "会写得更长"},
},
}},
{ID: "save", Title: "成稿", Kind: "artifact", Artifact: "mock-brief.md", Prompt: "篇幅:{{steps.length.output}}\n{{input}}"},
{ID: "note", Title: "收件", Kind: "card", Card: &core.ActivityCardTemplate{Kind: core.CardKindResult, Title: "演示工作流完成"}},
},
}
}
func truncateForJSON(s string, n int) string {
r := []rune(s)
if len(r) <= n {