package tools import ( "context" "encoding/json" "errors" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "sync/atomic" "testing" "time" agenttools "agent-native/runtime/tools" core "agent-system-core" ) var errToolActorDisabled = errors.New("tool actor disabled") type guardedToolContext struct { ToolExecCtx ctx context.Context check func() error } func (c guardedToolContext) Context() context.Context { if c.ctx != nil { return c.ctx } return context.Background() } func (c guardedToolContext) CheckExternalEffect() error { if c.check != nil { return c.check() } return nil } func TestHTTPToolChecksActorAndPropagatesCancellation(t *testing.T) { var calls atomic.Int32 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls.Add(1) <-r.Context().Done() })) defer srv.Close() exec := HTTPExecutor(core.HTTPToolSpec{Method: "POST", URL: srv.URL}, nil) denied := guardedToolContext{ToolExecCtx: dummyCtx{}, check: func() error { return errToolActorDisabled }} if _, err := exec(denied, nil); !errors.Is(err, errToolActorDisabled) || calls.Load() != 0 { t.Fatalf("disabled HTTP effect: %v, calls=%d", err, calls.Load()) } ctx, cancel := context.WithCancel(context.Background()) cancel() if _, err := exec(guardedToolContext{ToolExecCtx: dummyCtx{}, ctx: ctx}, nil); !errors.Is(err, context.Canceled) || calls.Load() != 0 { t.Fatalf("canceled HTTP effect: %v, calls=%d", err, calls.Load()) } } func TestHTTPToolCancelsInFlightRequest(t *testing.T) { started := make(chan struct{}) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { close(started) <-r.Context().Done() })) defer srv.Close() ctx, cancel := context.WithCancel(context.Background()) defer cancel() rc := guardedToolContext{ToolExecCtx: dummyCtx{}, ctx: ctx} done := make(chan error, 1) go func() { _, err := HTTPExecutor(core.HTTPToolSpec{Method: "GET", URL: srv.URL}, nil)(rc, nil) done <- err }() select { case <-started: case <-time.After(3 * time.Second): t.Fatal("HTTP request did not reach server") } cancel() select { case err := <-done: if !errors.Is(err, context.Canceled) { t.Fatalf("in-flight request cancellation: %v", err) } case <-time.After(3 * time.Second): t.Fatal("in-flight HTTP request ignored cancellation") } } type observedSearchProvider struct { calls int ctx context.Context } func (p *observedSearchProvider) Search(ctx context.Context, _ agenttools.SearchRequest) (agenttools.SearchResponse, error) { p.calls++ p.ctx = ctx return agenttools.SearchResponse{}, nil } func TestSearchToolChecksActorAndPassesRunContext(t *testing.T) { p := &observedSearchProvider{} r := NewRegistry() if err := RegisterSearchTools(r, p); err != nil { t.Fatal(err) } denied := guardedToolContext{ToolExecCtx: dummyCtx{}, check: func() error { return errToolActorDisabled }} if res := r.Execute(denied, core.ToolCallRequest{Name: "web_search", Arguments: json.RawMessage(`{"query":"q"}`)}); res.Status != "error" || !strings.Contains(res.Output, errToolActorDisabled.Error()) || p.calls != 0 { t.Fatalf("disabled search effect: %+v, calls=%d", res, p.calls) } ctx, cancel := context.WithCancel(context.Background()) cancel() if res := r.Execute(guardedToolContext{ToolExecCtx: dummyCtx{}, ctx: ctx}, core.ToolCallRequest{Name: "web_search", Arguments: json.RawMessage(`{"query":"q"}`)}); res.Status != "error" || p.calls != 0 { t.Fatalf("canceled search effect: %+v, calls=%d", res, p.calls) } if res := r.Execute(guardedToolContext{ToolExecCtx: dummyCtx{}}, core.ToolCallRequest{Name: "web_search", Arguments: json.RawMessage(`{"query":"q"}`)}); res.Status != "ok" || p.calls != 1 || p.ctx == nil { t.Fatalf("search context: %+v, calls=%d, context=%v", res, p.calls, p.ctx) } } func TestFSAndInterpreterCheckBeforeFilesystemEffects(t *testing.T) { root := t.TempDir() fsCtx := &fakeFSCtx{root: root, owner: "o", conv: "c", fsToolsOn: true} deniedFS := guardedToolContext{ToolExecCtx: fsCtx, check: func() error { return errToolActorDisabled }} if _, err := execFSWrite(deniedFS, json.RawMessage(`{"path":"note.md","content":"x"}`)); !errors.Is(err, errToolActorDisabled) { t.Fatalf("disabled fs write: %v", err) } if _, err := os.Stat(filepath.Join(root, "o", "c")); !os.IsNotExist(err) { t.Fatalf("disabled fs write created sandbox: %v", err) } execCtx := &execFake{root: root, owner: "o", conv: "c", execOn: true} deniedExec := guardedToolContext{ToolExecCtx: execCtx, check: func() error { return errToolActorDisabled }} if _, err := execShell(deniedExec, json.RawMessage(`{"script":"echo x"}`)); !errors.Is(err, errToolActorDisabled) { t.Fatalf("disabled shell write/start: %v", err) } if _, err := os.Stat(filepath.Join(root, "o", "c")); !os.IsNotExist(err) { t.Fatalf("disabled shell created sandbox: %v", err) } } func TestInterpreterRechecksBeforeProcessStart(t *testing.T) { root := t.TempDir() mark := filepath.Join(t.TempDir(), "executed") base := &execFake{root: root, owner: "o", conv: "c", execOn: true} checks := 0 rc := guardedToolContext{ToolExecCtx: base, check: func() error { checks++ if checks >= 4 { return errToolActorDisabled } return nil }} _, err := execShell(rc, json.RawMessage(`{"script":"touch `+mark+`"}`)) if !errors.Is(err, errToolActorDisabled) || checks < 4 { t.Fatalf("process start recheck: %v, checks=%d", err, checks) } if _, err := os.Stat(mark); !os.IsNotExist(err) { t.Fatalf("script ran after actor disable: %v", err) } } type guardedCodeContext struct{ guardedToolContext } func (guardedCodeContext) ExecuteTool(core.ToolCallRequest) core.ToolCallResult { return core.ToolCallResult{Status: "ok"} } func TestCodeOrchestrationChecksBeforeStarting(t *testing.T) { root := t.TempDir() base := &execFake{root: root, owner: "o", conv: "c", settings: map[string]bool{"runtime.codeOrchestration": true}} rc := guardedCodeContext{guardedToolContext{ToolExecCtx: base, check: func() error { return errToolActorDisabled }}} if _, err := execCodeScript(rc, json.RawMessage(`{"script":"print('hi')"}`)); !errors.Is(err, errToolActorDisabled) { t.Fatalf("disabled code orchestration: %v", err) } } func TestApplyUpperCheckedStopsBeforeWorkspaceWrite(t *testing.T) { dir := t.TempDir() upper := t.TempDir() write(t, filepath.Join(upper, "new.txt"), "new") if _, err := applyUpperChecked(context.Background(), func() error { return errToolActorDisabled }, upper, dir, map[string]string{}); !errors.Is(err, errToolActorDisabled) { t.Fatalf("disabled workspace apply: %v", err) } if _, err := os.Stat(filepath.Join(dir, "new.txt")); !os.IsNotExist(err) { t.Fatalf("disabled workspace apply wrote file: %v", err) } }