Files
Agent-Native/runtime/subagent/subagent_test.go
weichen d5a98e3f18 Add the delegation port and delegate_task tool backed by host factories
Why: complete agents delegate; the kernel must not nest executions
itself (orchestration stays with the host per the agent-execution
standard), so the missing piece is a port plus a tool face.

Evidence: go vet ./... clean; go test ./... 14 packages ok. Cases:
nil factory registers nothing, full delegation field round-trip
(goal/inputs/write_scope/non_goals/done_criteria/deliverables) with
sorted file list in the result text, sub-agent failure and factory
error propagation, schema-required goal rejection, pre-canceled
delegations never starting, and adapter end-to-end wiring through
Options.Subagent.

Impact: new runtime/subagent package (Request/Result/Factory); the
delegation message set matches the agent-execution delegation clause
(sub-tasks derive no extra permissions; budget fields are host-set);
native.Options gains Subagent.

Refs: plan B8; standards/agent-execution.md delegation section.
2026-10-01 05:53:07 +00:00

87 lines
3.0 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"]
}`))
if res.IsError {
t.Fatal(res.Content)
}
if got.Goal != "调研主题 X" || got.WriteScope != "research/" || got.DoneCriteria != "产出三来源对照表" {
t.Fatalf("delegation fields 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)
}
}