Why: the delegation task brief must state goal, output format, tools and boundaries (Q3 acceptance); Request had no carriers for the two new sections, so delegate_task callers could not declare them. Evidence: TestDelegateToolRoundTrip extended to assert output_format and tools survive the tool-call round trip; runtime suite green (SELF); CI authoritative (GATE). Impact: additive Request fields + delegate_task schema/description; empty fields keep today's behavior (task brief sections omitted). Refs: Q3 plan WP3; Dev card .task/feature-agent-stack-q3.md
117 lines
4.2 KiB
Go
117 lines
4.2 KiB
Go
package subagent
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
"agent-native/runtime/tools"
|
|
)
|
|
|
|
func TestRegisterNilFactoryNoop(t *testing.T) {
|
|
reg := tools.NewRegistry(t.TempDir())
|
|
Register(reg, nil)
|
|
for _, d := range reg.Decls() {
|
|
if d.Name == "delegate_task" {
|
|
t.Fatal("nil factory must not register delegate_task")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDelegateToolRoundTrip(t *testing.T) {
|
|
var got Request
|
|
reg := tools.NewRegistry(t.TempDir())
|
|
Register(reg, func(ctx context.Context, req Request) (Result, error) {
|
|
got = req
|
|
return Result{
|
|
Success: true,
|
|
Text: "调研完成:结论 A。",
|
|
Files: map[string][]byte{"notes.md": []byte("n"), "data/table.csv": []byte("c")},
|
|
}, nil
|
|
})
|
|
res := reg.Execute(context.Background(), "delegate_task", json.RawMessage(`{
|
|
"goal":"调研主题 X",
|
|
"inputs":"docs/v2 规范",
|
|
"write_scope":"research/",
|
|
"non_goals":"不做实现",
|
|
"done_criteria":"产出三来源对照表",
|
|
"deliverables":["notes.md","data/table.csv"],
|
|
"output_format":"Markdown:结论先行,附三来源对照表",
|
|
"tools":["web_search","read_file"]
|
|
}`))
|
|
if res.IsError {
|
|
t.Fatal(res.Content)
|
|
}
|
|
if got.Goal != "调研主题 X" || got.WriteScope != "research/" || got.DoneCriteria != "产出三来源对照表" {
|
|
t.Fatalf("delegation fields lost: %+v", got)
|
|
}
|
|
// Q3 WP3:输出格式与工具面随委派请求透传(任务书两节的声明源)。
|
|
if got.OutputFormat != "Markdown:结论先行,附三来源对照表" ||
|
|
strings.Join(got.Tools, ",") != "web_search,read_file" {
|
|
t.Fatalf("output_format/tools lost: %+v", got)
|
|
}
|
|
if !strings.Contains(res.Content, "[sub-agent succeeded]") ||
|
|
!strings.Contains(res.Content, "调研完成") ||
|
|
!strings.Contains(res.Content, "files: data/table.csv, notes.md") {
|
|
t.Fatalf("result text wrong:\n%s", res.Content)
|
|
}
|
|
}
|
|
|
|
func TestDelegateToolFailureAndValidation(t *testing.T) {
|
|
reg := tools.NewRegistry(t.TempDir())
|
|
Register(reg, func(ctx context.Context, req Request) (Result, error) {
|
|
return Result{Success: false, Err: "子任务超出预算"}, nil
|
|
})
|
|
res := reg.Execute(context.Background(), "delegate_task", json.RawMessage(`{"goal":"g","done_criteria":"d"}`))
|
|
if !res.IsError || !strings.Contains(res.Content, "[sub-agent failed]") || !strings.Contains(res.Content, "子任务超出预算") {
|
|
t.Fatalf("failure must propagate, got %+v", res)
|
|
}
|
|
// goal 必填(schema 校验拦截)。
|
|
res = reg.Execute(context.Background(), "delegate_task", json.RawMessage(`{"done_criteria":"d"}`))
|
|
if !res.IsError || !strings.Contains(res.Content, "required argument goal") {
|
|
t.Fatalf("missing goal must fail schema validation, got %+v", res)
|
|
}
|
|
}
|
|
|
|
func TestDelegateToolFactoryErrorAndCancel(t *testing.T) {
|
|
reg := tools.NewRegistry(t.TempDir())
|
|
Register(reg, func(ctx context.Context, req Request) (Result, error) {
|
|
return Result{}, context.Canceled
|
|
})
|
|
res := reg.Execute(context.Background(), "delegate_task", json.RawMessage(`{"goal":"g","done_criteria":"d"}`))
|
|
if !res.IsError || !strings.Contains(res.Content, "sub-agent failed") {
|
|
t.Fatalf("factory error must surface, got %+v", res)
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
res = reg.Execute(ctx, "delegate_task", json.RawMessage(`{"goal":"g","done_criteria":"d"}`))
|
|
if !res.IsError || !strings.Contains(res.Content, "canceled before start") {
|
|
t.Fatalf("pre-canceled delegation must not start, got %+v", res)
|
|
}
|
|
}
|
|
|
|
func TestCapParentTextWritesOverflowPath(t *testing.T) {
|
|
shown, full := CapParentText("短")
|
|
if shown != "短" || full != nil {
|
|
t.Fatalf("short: %q %d", shown, len(full))
|
|
}
|
|
body := strings.Repeat("字", ParentResultCap+3)
|
|
shown, full = CapParentText(body)
|
|
if full == nil || string(full) != body {
|
|
t.Fatal("full text not returned")
|
|
}
|
|
if len([]rune(shown)) != ParentResultCap {
|
|
t.Fatalf("shown runes %d", len([]rune(shown)))
|
|
}
|
|
reg := tools.NewRegistry(t.TempDir())
|
|
Register(reg, func(ctx context.Context, req Request) (Result, error) {
|
|
return Result{Success: true, Text: body}, nil
|
|
})
|
|
res := reg.Execute(context.Background(), "delegate_task", json.RawMessage(`{"goal":"g","done_criteria":"d"}`))
|
|
if res.IsError || !strings.Contains(res.Content, "subagent-result.txt") {
|
|
t.Fatalf("capped result: %+v", res)
|
|
}
|
|
}
|