Files
Agent-Native/adapters/native/subagent_writable_test.go
T
weichen f2fb9ffa6d Reject subagent delegation under a Writable write-set
Why: review M5 - delegate_task spawned sub-agents outside the parent's
Writable enforcement, the same unenforceability preflight already
rejects for exec/MCP/host tools.
Evidence: preflight rejection, delegation-still-works and legacy
cases pass (verify-m5.json, verify-m5-host.json exit=0).
Impact: preflight check only; empty Writable behavior unchanged.
Refs: feature/audit-followups-pin-gate-m5
2026-10-22 06:17:26 +00:00

113 lines
3.7 KiB
Go

package native
import (
"context"
"os"
"path/filepath"
"testing"
"agent-native/exec"
"agent-native/runtime/llm"
"agent-native/runtime/subagent"
)
func TestContractWritableRejectsSubagentInPreflight(t *testing.T) {
dir := t.TempDir()
streamCalls := 0
inner := script(llm.Message{Role: llm.RoleAssistant, Content: "x", StopReason: llm.StopEnd})
factoryCalls := 0
b := New(Options{
ModelOptions: ModelOptions{
Model: "fake",
Stream: func(ctx context.Context, model string, messages []llm.Message, decls []llm.ToolDecl, opts llm.StreamOptions) <-chan llm.Event {
streamCalls++
return inner(ctx, model, messages, decls, opts)
},
},
ToolOptions: ToolOptions{
Subagent: func(ctx context.Context, req subagent.Request) (subagent.Result, error) {
factoryCalls++
return subagent.Result{Success: true}, nil
},
},
}).Contract()
res, err := b.Execute(context.Background(), exec.ContractRequest{
ExecutionID: "e-sub",
Prompt: "x",
Workspace: exec.WorkspaceHandle{
Root: dir,
Writable: []string{"a.md"},
},
ToolPolicy: exec.ToolPolicy{Profile: exec.ToolProfileWorkspace},
Targets: []string{"a.md"},
})
if err != nil {
t.Fatal(err)
}
if res.Outcome == exec.OutcomeSucceeded {
t.Fatal("Writable + subagent delegation enabled must be rejected in preflight")
}
const want = "writable write-set cannot be enforced when subagent delegation is enabled"
if res.Err == nil || res.Err.Reason != want {
t.Fatalf("want reason %q, got %+v", want, res.Err)
}
if streamCalls != 0 || factoryCalls != 0 {
t.Fatalf("preflight must stop before model/factory: stream=%d factory=%d", streamCalls, factoryCalls)
}
if _, err := os.Stat(filepath.Join(dir, "a.md")); !os.IsNotExist(err) {
t.Fatal("deliverable a.md must not be created when preflight rejects")
}
if _, err := os.Stat(filepath.Join(dir, ".agentx")); !os.IsNotExist(err) {
t.Fatal("no session state must be written when preflight rejects")
}
}
func TestContractSubagentWithoutWritableStillDelegates(t *testing.T) {
dir := t.TempDir()
var delegated *subagent.Request
stream, c := captureStream(
llm.Message{Role: llm.RoleAssistant, StopReason: llm.StopToolCalls, ToolCalls: []llm.ToolCall{{
ID: "c1", Name: "delegate_task",
Arguments: `{"goal":"查证数据","write_scope":"research/","done_criteria":"三来源"}`,
}}},
llm.Message{Role: llm.RoleAssistant, StopReason: llm.StopToolCalls, ToolCalls: []llm.ToolCall{{
ID: "c2", Name: "write_file",
Arguments: `{"path":"output.md","content":"done"}`,
}}},
llm.Message{Role: llm.RoleAssistant, Content: "ok", StopReason: llm.StopEnd},
)
b := New(Options{
ModelOptions: ModelOptions{Model: "fake", Stream: stream},
ToolOptions: ToolOptions{
Subagent: func(ctx context.Context, req subagent.Request) (subagent.Result, error) {
delegated = &req
return subagent.Result{Success: true, Text: "结论已查明"}, nil
},
},
}).Contract()
res, err := b.Execute(context.Background(), exec.ContractRequest{
ExecutionID: "e-sub-ok",
Prompt: "分工",
Workspace: exec.WorkspaceHandle{Root: dir},
ToolPolicy: exec.ToolPolicy{Profile: exec.ToolProfileWorkspace},
Targets: []string{"output.md"},
})
if err != nil || res.Outcome != exec.OutcomeSucceeded {
t.Fatalf("execute: err=%v res=%+v", err, res)
}
if delegated == nil || delegated.Goal != "查证数据" {
t.Fatalf("factory not invoked with fields: %+v", delegated)
}
sawDelegate := false
for _, names := range c.toolDecls {
for _, n := range names {
if n == "delegate_task" {
sawDelegate = true
}
}
}
if !sawDelegate {
t.Fatal("delegate_task must be declared when Subagent factory is set and Writable is empty")
}
}