mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 06:17:41 +00:00
* feat(collaboration): isolate delegated artifacts and child runs Isolate delegated inputs and outputs behind secure artifact exchange lifecycles. Scope Agent Link tasks by tenant and root agent, and enforce delegation spawn-tree boundaries. Add process-wide child-run admission and preserve logical media paths across native, MCP, and sandbox execution. * fix(collaboration): harden delegated task isolation Enforce tenant and root-agent task scope across migrations and stores. Add exactly-once async completion delivery, delegated sandbox boundaries, and confined artifact and media recovery across runtime surfaces. * fix(collaboration): recover interrupted async tasks * fix(collaboration): normalize persisted child-run status --------- Co-authored-by: ntduc <ntduc@cpp.ai.vn>
81 lines
2.6 KiB
Go
81 lines
2.6 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
)
|
|
|
|
func delegationArtifactTestContext() context.Context {
|
|
ctx := store.WithTenantID(context.Background(), store.MasterTenantID)
|
|
ctx = WithDelegationID(ctx, "11111111-1111-1111-1111-111111111111")
|
|
return WithDelegationArtifactInputs(ctx, "/runtime/delegations/input")
|
|
}
|
|
|
|
func TestDelegationArtifactRunRejectsAsyncChildModes(t *testing.T) {
|
|
ctx := delegationArtifactTestContext()
|
|
for _, mode := range []string{"async", "spawn", "background"} {
|
|
if err := validateDelegationChildRunMode(ctx, "delegate", mode); err == nil {
|
|
t.Fatalf("mode %q was accepted", mode)
|
|
}
|
|
}
|
|
if err := validateDelegationChildRunMode(ctx, "delegate", "sync"); err != nil {
|
|
t.Fatalf("sync mode rejected: %v", err)
|
|
}
|
|
if err := validateDelegationChildRunMode(context.Background(), "delegate", "async"); err != nil {
|
|
t.Fatalf("normal async run rejected: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSpawnToolRejectsDefaultAsyncInsideDelegationArtifactRun(t *testing.T) {
|
|
tool := NewSpawnTool(nil, "parent", 0)
|
|
result := tool.Execute(delegationArtifactTestContext(), map[string]any{
|
|
"task": "must not escape the delegation lifetime",
|
|
})
|
|
if result == nil || !strings.Contains(result.ForLLM, "mode=\"sync\"") {
|
|
t.Fatalf("unexpected result: %#v", result)
|
|
}
|
|
}
|
|
|
|
func TestDelegateToolRejectsAsyncInsideDelegationArtifactRunBeforeLookup(t *testing.T) {
|
|
tool := &DelegateTool{}
|
|
result := tool.Execute(delegationArtifactTestContext(), map[string]any{
|
|
"agent_key": "child",
|
|
"task": "must not escape the delegation lifetime",
|
|
"mode": "async",
|
|
})
|
|
if result == nil || !strings.Contains(result.ForLLM, "mode=\"sync\"") {
|
|
t.Fatalf("unexpected result: %#v", result)
|
|
}
|
|
}
|
|
|
|
func TestDelegateToolSchemaKeepsBoundedInputsOptional(t *testing.T) {
|
|
schema := (&DelegateTool{}).Parameters()
|
|
properties, ok := schema["properties"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("properties = %#v", schema["properties"])
|
|
}
|
|
inputs, ok := properties["inputs"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("inputs schema = %#v", properties["inputs"])
|
|
}
|
|
if got := inputs["type"]; got != "array" {
|
|
t.Fatalf("inputs type = %#v, want array", got)
|
|
}
|
|
if got := inputs["maxItems"]; got != DelegationArtifactMaxFiles {
|
|
t.Fatalf("inputs maxItems = %#v, want %d", got, DelegationArtifactMaxFiles)
|
|
}
|
|
if required, ok := schema["required"].([]string); ok {
|
|
for _, name := range required {
|
|
if name == "inputs" {
|
|
t.Fatal("inputs unexpectedly became required")
|
|
}
|
|
}
|
|
}
|
|
if _, ok := properties["delegation_id"]; !ok {
|
|
t.Fatal("schema omitted durable delegation result lookup")
|
|
}
|
|
}
|