Files
Agent-Native/runtime/subagent/subagent_test.go
T
weichen cdbdeb0b14
ci / vet-and-test (pull_request) Successful in 51s
ci / docker-checkout-hostgateway (pull_request) Successful in 2s
Carry output format and tool surface on delegation requests (Q3 WP3)
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
2026-10-08 03:27:09 +00:00

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)
}
}