Limit paginated result set to a maximum number of posts

Hope I understand this correctly. Below is an approach using few hooks to modify the query before fetching the data. Might need some adjustments and testing.

Note that numberposts is just an alias for posts_per_page in get_posts() wrapper of WP_Query.

We could try to use paged instead of offset, but it will not work if we plan to modify posts_per_page on the last page calculated by ceil($max_posts/$posts_per_page ). So we set offset directly as:

$args = array(
  'posts_per_page' => $posts_per_page,
  'offset'         => ($page - 1) * $posts_per_page,
  'post_status'    => array( 'publish' ),
);

We can adjust the number of found posts if it exceeds $max_posts:

$fn_found_posts = function( $found_posts, $q ) use ( $max_posts, $page ) { 
    return $found_posts > $max_posts ? $max_posts : $found_posts;
};

add_action( 'pre_get_posts', $fn_pre_get_posts );

but this filtering will not adjust the returned posts array.

We can therefore adjust the posts_per_page to $max_posts - ($page - 1) * $posts_per_page for the last page (according to $max_posts) because that will adjust the previously set offset:

$fn_pre_get_posts = function( $q ) use ($page, $posts_per_page, $max_posts ) {  
    if ( $page == ceil($max_posts/$posts_per_page ) ) {
        $q->set( 'posts_per_page', $max_posts - ($page - 1) * $posts_per_page );
    }
};

add_filter( 'found_posts', $fn_found_posts );

We can then return empty array for pages after the last page (according to $max_posts):

$fn_posts_pre_query = function( $posts ) use ($page, $posts_per_page, $max_posts ) { 
    return $page > ceil( $max_posts/$posts_per_page) ? array() : $posts; 
};

add_filter( 'posts_pre_query', $fn_posts_pre_query );

Here the modified getQuery() function:

function getQuery(int $page, int $posts_per_page = 2, int $max_posts = 7): \WP_Query {
    $fn_found_posts = function( $found_posts ) use ( $max_posts ) { 
        return $found_posts > $max_posts ? $max_posts : $found_posts;
    };

    $fn_posts_pre_query = function( $posts ) use ($page, $posts_per_page, $max_posts ) { 
        return $page > ceil($max_posts/$posts_per_page) ? array() : $posts; 
    };
    
    $fn_pre_get_posts = function( $q ) use ($page, $posts_per_page, $max_posts ) {  
        if ( $page == ceil($max_posts/$posts_per_page ) ) {
            $q->set( 'posts_per_page', $max_posts - ($page - 1) * $posts_per_page );
        }
    };
    
    add_filter( 'found_posts', $fn_found_posts );
    add_filter( 'posts_pre_query', $fn_posts_pre_query );
    add_action( 'pre_get_posts', $fn_pre_get_posts );

    $args = array(
        'posts_per_page' => $posts_per_page,
        'offset'         => ($page - 1) * $posts_per_page,
        'post_status'    => array( 'publish' ),
    );
    $q = new \WP_Query($args);

    remove_action( 'pre_get_posts', $fn_pre_get_posts );
    remove_filter( 'posts_pre_query', $fn_empty_array );
    remove_filter( 'found_posts', $fn_found_posts );

    return $q;
}

ps: one might add some input range checks etc.