package mcp import ( "context" "errors" "strings" "testing" "time" "cyberstrike-ai/internal/authctx" "go.uber.org/zap" ) func TestToolAuthorizerIsUniversalAndExecutionKeepsOwner(t *testing.T) { server := NewServer(zap.NewNop()) server.RegisterTool(Tool{Name: "echo", InputSchema: map[string]interface{}{"type": "object"}}, func(ctx context.Context, args map[string]interface{}) (*ToolResult, error) { return &ToolResult{Content: []Content{{Type: "text", Text: "ok"}}}, nil }) server.SetToolAuthorizer(func(ctx context.Context, toolName string, args map[string]interface{}) error { if _, ok := authctx.PrincipalFromContext(ctx); !ok { return errors.New("principal required") } return nil }) _, deniedExecutionID, err := server.CallTool(context.Background(), "echo", nil) if err == nil { t.Fatal("tool call without principal was allowed") } if deniedExecutionID == "" { t.Fatal("denied tool call should still return an execution id") } deniedExecution, ok := server.GetExecution(deniedExecutionID) if !ok || deniedExecution == nil { t.Fatalf("missing denied execution %q", deniedExecutionID) } if deniedExecution.Status != ToolExecutionStatusFailed || !strings.Contains(deniedExecution.Error, "principal required") { t.Fatalf("denied execution = %#v, want failed with authorization error", deniedExecution) } ctx := authctx.WithPrincipal(context.Background(), authctx.NewPrincipal("u1", "user", "assigned", map[string]bool{"mcp:execute": true})) _, executionID, err := server.CallTool(ctx, "echo", nil) if err != nil { t.Fatal(err) } execution, ok := server.GetExecution(executionID) if !ok || execution.OwnerUserID != "u1" { t.Fatalf("execution owner = %#v, want u1", execution) } } func TestServerCallToolBoundedWaitForInternalTool(t *testing.T) { server := NewServer(zap.NewNop()) server.toolWaitTimeout = 10 * time.Millisecond release := make(chan struct{}) started := make(chan struct{}) server.RegisterTool(Tool{Name: "slow", InputSchema: map[string]interface{}{"type": "object"}}, func(ctx context.Context, args map[string]interface{}) (*ToolResult, error) { close(started) select { case <-release: return &ToolResult{Content: []Content{{Type: "text", Text: "internal done"}}}, nil case <-ctx.Done(): return nil, ctx.Err() } }) callCtx, callCancel := context.WithCancel(context.Background()) result, executionID, err := server.CallTool(callCtx, "slow", nil) if err != nil { t.Fatalf("CallTool returned error: %v", err) } if executionID == "" || result == nil || !result.IsError { t.Fatalf("expected soft timeout with execution id, result=%#v id=%q", result, executionID) } if text := ToolResultPlainText(result); !strings.Contains(text, executionID) || !strings.Contains(text, "wait_tool_execution") { t.Fatalf("timeout result missing execution guidance: %q", text) } select { case <-started: default: t.Fatal("internal worker did not start") } callCancel() close(release) snapshot, err := server.executionService.Wait(context.Background(), executionID, time.Second) if err != nil { t.Fatalf("wait internal execution: %v", err) } if snapshot == nil || snapshot.Execution == nil || snapshot.Execution.Status != ToolExecutionStatusCompleted { t.Fatalf("snapshot = %#v, want completed", snapshot) } if got := ToolResultPlainText(snapshot.Execution.Result); got != "internal done" { t.Fatalf("result = %q, want internal done", got) } } func TestWaitToolExecutionWaitsForInternalActiveExecution(t *testing.T) { server := NewServer(zap.NewNop()) server.toolWaitTimeout = 10 * time.Millisecond release := make(chan struct{}) server.RegisterTool(Tool{Name: "slow", InputSchema: map[string]interface{}{"type": "object"}}, func(ctx context.Context, args map[string]interface{}) (*ToolResult, error) { select { case <-release: return &ToolResult{Content: []Content{{Type: "text", Text: "wait saw completion"}}}, nil case <-ctx.Done(): return nil, ctx.Err() } }) RegisterExecutionControlTools(server, nil) result, executionID, err := server.CallTool(context.Background(), "slow", nil) if err != nil { t.Fatalf("CallTool returned error: %v", err) } if result == nil || !result.IsError || executionID == "" { t.Fatalf("expected initial bounded wait timeout, result=%#v id=%q", result, executionID) } done := make(chan *ToolResult, 1) errCh := make(chan error, 1) go func() { waitResult, _, waitErr := server.CallTool(context.Background(), "wait_tool_execution", map[string]interface{}{ "execution_id": executionID, "timeout_seconds": 1, }) if waitErr != nil { errCh <- waitErr return } done <- waitResult }() select { case <-done: t.Fatal("wait_tool_execution returned before target execution completed") case err := <-errCh: t.Fatalf("wait_tool_execution errored before release: %v", err) case <-time.After(50 * time.Millisecond): } close(release) select { case err := <-errCh: t.Fatalf("wait_tool_execution returned error: %v", err) case waitResult := <-done: if waitResult == nil || waitResult.IsError { t.Fatalf("expected successful wait result, got %#v", waitResult) } if body := ToolResultPlainText(waitResult); !strings.Contains(body, "wait saw completion") || !strings.Contains(body, `"status": "completed"`) { t.Fatalf("wait result missing completed target: %s", body) } case <-time.After(time.Second): t.Fatal("wait_tool_execution did not return after target completion") } } func TestWaitToolExecutionTimeoutIsObservationNotFailure(t *testing.T) { server := NewServer(zap.NewNop()) server.toolWaitTimeout = 10 * time.Millisecond release := make(chan struct{}) server.RegisterTool(Tool{Name: "slow_observed", InputSchema: map[string]interface{}{"type": "object"}}, func(ctx context.Context, args map[string]interface{}) (*ToolResult, error) { <-release return &ToolResult{Content: []Content{{Type: "text", Text: "done"}}}, nil }) RegisterExecutionControlTools(server, nil) result, executionID, err := server.CallTool(context.Background(), "slow_observed", nil) if err != nil { t.Fatalf("CallTool returned error: %v", err) } if result == nil || !result.IsError || executionID == "" { t.Fatalf("expected initial bounded wait timeout, result=%#v id=%q", result, executionID) } waitResult, _, err := server.CallTool(context.Background(), "wait_tool_execution", map[string]interface{}{ "execution_id": executionID, "timeout_seconds": 0.01, }) if err != nil { t.Fatalf("wait_tool_execution returned error: %v", err) } if waitResult == nil { t.Fatal("missing wait result") } if waitResult.IsError { t.Fatalf("wait timeout should be a successful observation, got %#v", waitResult) } body := ToolResultPlainText(waitResult) if !strings.Contains(body, `"status": "running"`) || !strings.Contains(body, "本次等待已到达") { t.Fatalf("wait timeout body missing running status/guidance: %s", body) } close(release) } func TestGetToolExecutionIncludesBoundedPartialOutput(t *testing.T) { server := NewServer(zap.NewNop()) RegisterExecutionControlTools(server, nil) executionID := server.BeginToolExecution(context.Background(), "execute", map[string]interface{}{"command": "demo"}) if executionID == "" { t.Fatal("missing execution id") } server.AppendToolExecutionPartialOutput(executionID, "first\n") server.AppendToolExecutionPartialOutput(executionID, strings.Repeat("x", 32)) result, _, err := server.CallTool(context.Background(), "get_tool_execution", map[string]interface{}{ "execution_id": executionID, "partial_output_max_bytes": 8, }) if err != nil { t.Fatalf("get_tool_execution: %v", err) } body := ToolResultPlainText(result) if !strings.Contains(body, `"partial_output": "xxxxxxxx"`) { t.Fatalf("missing bounded partial output: %s", body) } if !strings.Contains(body, `"partial_output_bytes": 38`) { t.Fatalf("missing partial byte count: %s", body) } result, _, err = server.CallTool(context.Background(), "get_tool_execution", map[string]interface{}{ "execution_id": executionID, "include_partial_output": false, }) if err != nil { t.Fatalf("get_tool_execution without partial: %v", err) } if body := ToolResultPlainText(result); strings.Contains(body, "partial_output") { t.Fatalf("partial output should be omitted: %s", body) } }